Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0b6b14e89 | ||
|
|
2734c2383a | ||
|
|
74dc1265a8 | ||
|
|
678d77fb91 | ||
|
|
97c601cd7f | ||
|
|
a93e261120 | ||
|
|
72f1cf50ae | ||
|
|
6821f6ee12 | ||
|
|
85d274f2ef | ||
|
|
235e3c08b7 |
@@ -2,6 +2,9 @@
|
||||
|
||||
Ce dépôt contient le pack Sanctuary et ses mods. Lire `README.md`, puis le ticket concerné.
|
||||
La vision est dans `docs/vision.md` ; elle décrit aussi des fonctionnalités futures.
|
||||
Pour développer/livrer le site, packwiz, la Beta dédiée et Prism local, lire
|
||||
`docs/updates.md`. La compétence personnelle `sanctuary-updates` mémorise les
|
||||
chemins et accès propres au poste ; ne pas déduire une publication d'un simple push.
|
||||
|
||||
- Avant une modification, lire l'état Git et préserver les changements existants.
|
||||
- Un ticket correspond à un résultat jouable ou vérifiable et à une branche `codex/<sujet>`.
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
# beta.293 — Happy Creepers, logo packwiz et mods facultatifs
|
||||
|
||||
- Intégration de la branche `codex/happy-creepers` avec les baleines beta.290.
|
||||
- Logo Sanctuary pendant packwiz et outils prêts dans le ZIP Prism.
|
||||
- Simple Voice Chat, Bobby facultatif, Lithium et Entity Culling épinglés pour 26.3.
|
||||
- Même canal et même instance Prism, profils client/serveur et sauvegardes conservés.
|
||||
- [Contrat et reçu de livraison](docs/release-beta293.md).
|
||||
|
||||
# beta.290 — Baleine fantôme volante
|
||||
|
||||
- Import de l’IA, du modèle animé, de la texture et des effets de la baleine Sanctuary 26.2.
|
||||
- Deux baleines chargées pour le serveur entier, toutes dimensions et sources d’apparition comprises.
|
||||
- Œuf créatif personnalisé depuis la texture native `egg.png` de Minecraft 26.3, silhouette et ombres conservées.
|
||||
- Refus d’un troisième œuf avant consommation, libellés FR/EN et calendrier propre à chaque serveur.
|
||||
- [Contrat et vérifications](docs/flying-whale-beta290.md).
|
||||
|
||||
# beta.278 — Duel Battle pour un claim consenti
|
||||
|
||||
- Proposition depuis le clic droit d’un claim déjà revendiqué ; propriétaire et challenger valident le même enjeu.
|
||||
|
||||
@@ -1,26 +1,174 @@
|
||||
# Sanctuary
|
||||
|
||||
Le [téléchargement direct Sanctuary Beta](docs/beta-download.md) fournit une
|
||||
instance Prism prête : importer le ZIP, puis lancer pour télécharger le pack.
|
||||
Les publications du canal restent commandées par le créateur via Codex.
|
||||
|
||||
## beta.280 — JEI réintégré et packwiz serveur
|
||||
|
||||
[Un seul JAR Sanctuary avec le fork JEI et un profil packwiz serveur](docs/jei-server-beta280.md).
|
||||
Client et serveur partagent le même artefact ; le profil serveur ne contient
|
||||
que Sanctuary et Fabric API. Le [kit Linux](docs/server-pack.md) prépare les
|
||||
mises à jour et la connexion aux services Web existants. Build, 13 GameTests,
|
||||
sept contrôles de l'outil de mise à jour, installation packwiz et démarrage du
|
||||
JAR de production vérifiés. La nouvelle CT 350 sur `gpu` accueille le serveur
|
||||
de 20 places, relié à l'API, à la communauté et à la console privée du site.
|
||||
[Release beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.280).
|
||||
|
||||
Les **quatre expansions validées sont disponibles dans Sanctuary normal** :
|
||||
Nord glacial, Sud des canyons, Ouest océanique et Est volcanique.
|
||||
Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
|
||||
correspondantes. Le module Sanctuary Test n’est pas requis.
|
||||
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
|
||||
|
||||
|
||||
## beta.295 — Serveur Sanctuary dans la liste multijoueur
|
||||
|
||||
[Serveur préenregistré et logo du pack](docs/default-server-beta295.md) :
|
||||
ajout initial de **Sanctuary Beta** sans écraser les favoris personnels,
|
||||
avec icône 64 × 64 également distribuée au serveur. Intègre la simplification
|
||||
de `/sanctuary property`. Build, 18 tests serveur et parcours Vulkan FR/EN
|
||||
réussis. [Livraison complète effectuée](docs/release-beta295.md) : serveur,
|
||||
téléchargement Web et instance Prism historique en beta.295.
|
||||
|
||||
## beta.293 — mise à jour complète
|
||||
|
||||
[Livraison complète publiée](docs/release-beta293.md) : Happy Creepers de la
|
||||
branche dédiée, baleines fantômes, logo packwiz et quatre mods facultatifs.
|
||||
Serveur Beta, téléchargement Web et même instance Prism synchronisés en
|
||||
**beta.293**, sauvegardes et réglages conservés. Build et 79 tests serveur
|
||||
réussis ; assertions Vulkan réussies, blocage de fermeture de l'outil de test
|
||||
consigné. Réseau vocal contrôlé, essai audible à deux joueurs encore ouvert.
|
||||
|
||||
## beta.292 — voix et mods facultatifs (préparation locale)
|
||||
|
||||
[Simple Voice Chat, Bobby, Lithium et Entity Culling rejoignent le pack](docs/optional-mods-beta292.md).
|
||||
Les quatre restent décochables côté client ; Lithium et la voix rejoignent
|
||||
aussi le profil serveur. Logo packwiz conservé, 46 tests serveur ciblés et
|
||||
parcours Vulkan avec et sans Sodium réussis. Préparation locale, sans publication ni
|
||||
modification de l'instance personnelle. Dozei Stream et Only Fun restent en tickets.
|
||||
|
||||
## beta.291 — logo Sanctuary dans packwiz (préparation locale)
|
||||
|
||||
[Le bandeau fourni s'affiche pendant les téléchargements packwiz](docs/packwiz-logo-beta291.md),
|
||||
dans l'instance Prism préparée. L'installateur embarque le logo et conserve
|
||||
le moteur packwiz officiel. Les [tickets des prochaines intégrations](docs/integrations-mods-next.md)
|
||||
couvrent Simple Voice Chat, Bobby facultatif, Lithium, Entity Culling,
|
||||
Dozei Stream et Only Fun ; ces mods ne sont pas ajoutés dans cette version.
|
||||
Fenêtre FR/EN, contrôles d'archive et deux synchronisations isolées réussis.
|
||||
Construction ciblée avec 13 tests serveur réussie ; un test hérité du quota
|
||||
de baleines reste en échec, détaillé dans le contrat. Aucune publication.
|
||||
|
||||
## beta.290 — baleine fantôme volante (branche locale)
|
||||
|
||||
[Import direct de la baleine 26.2](docs/flying-whale-beta290.md), avec son IA,
|
||||
son rendu nuageux animé et son œuf personnalisé à partir de `egg.png` de
|
||||
Minecraft. Deux baleines chargées au maximum pour le serveur, œuf refusé
|
||||
sans consommation si les places sont prises. Construction, 19 tests serveur
|
||||
et parcours Vulkan avec Sodium réussis. Préparée localement, sans publication
|
||||
ni mise à jour d’installation.
|
||||
|
||||
## beta.289 — mise à jour complète
|
||||
|
||||
[Livraison des zombies et de la lumière portée](docs/release-beta289.md), avec
|
||||
JEI intégré au même JAR pour le client et le serveur. Le téléchargement du
|
||||
site conserve l’instance Prism prête à suivre le canal stable.
|
||||
**Publiée** : serveur Beta, téléchargement Web et instance Prism personnelle
|
||||
synchronisés en beta.289, avec sauvegardes et réglages conservés.
|
||||
[Procédure de développement et de publication](docs/updates.md).
|
||||
Construction client/serveur, **31 tests serveur** et parcours des zombies et
|
||||
familiers sous **Vulkan avec Sodium** réussis pour cette livraison.
|
||||
|
||||
## beta.288 — correction des lumières portées intégrée
|
||||
|
||||
[Le correctif des Dynamic Lights rejoint le pack courant](docs/dynamic-hand-light-beta288.md).
|
||||
Les torches et lanternes tenues en main éclairent le terrain avec Sodium,
|
||||
avec ou sans PBR. Les zombies et les neuf tenues de beta.287 sont conservés.
|
||||
Build, **19 tests serveur** et trois parcours **Vulkan** réussis : éclairage
|
||||
avec/sans Sodium, puis zombies et familiers avec Sodium.
|
||||
[Pack local beta.288](build/Sanctuary-beta.288.mrpack).
|
||||
|
||||
## beta.287 — tenues de zombies et postures
|
||||
|
||||
[Neuf tenues aléatoires et Hunter accroupi](docs/zombie-appearances-beta287.md)
|
||||
pour le zombie classique et les six infectés. Les PNG fournis par le créateur
|
||||
sont choisis aléatoirement par individu et gardés au rechargement, y compris
|
||||
pour les familiers. Tous gardent les bras classiques en avant ; le Hunter
|
||||
conserve sa marche accroupie.
|
||||
Les familiers infectés conservent leur modèle adulte miniature à taille variable.
|
||||
Build, **77 régressions, 19 tests Smoker et 16 tests Hunter** en sessions
|
||||
séparées, ainsi que le parcours **Vulkan**, réussis. Neuf tenues par type,
|
||||
bébé classique, 21 combinaisons de familiers, rééquipement stable et marche/bond
|
||||
du Hunter vérifiés. [Pack local beta.287](build/Sanctuary-beta.287.mrpack).
|
||||
|
||||
## beta.286 — Hunter et bond offensif
|
||||
|
||||
[Sixième infecté](docs/hunter-beta286.md) : préparation accroupie, bond physique
|
||||
vers le joueur et frappe unique au contact. Sa trajectoire reste fixe après
|
||||
le départ : une esquive, un mur ou un plafond peuvent faire rater l’attaque.
|
||||
T-shirt bleu et pantalon anthracite, œuf créatif et familier adulte miniature
|
||||
à taille variable, avec son propre bond offensif sur G.
|
||||
Build, **16 tests Hunter, 19 tests Smoker et 77 régressions**, en sessions séparées,
|
||||
ainsi que le parcours **Vulkan FR/EN GUI 2/3/4**, réussis. Contact contre un vrai
|
||||
joueur, esquive au clavier, atterrissage et trois tailles vérifiés.
|
||||
[Pack local beta.286](build/Sanctuary-beta.286.mrpack).
|
||||
|
||||
## beta.285 — Smoker et lasso
|
||||
|
||||
[Cinquième infecté](docs/smoker-beta285.md) : un lasso physique qui peut rater.
|
||||
Il doit toucher sa cible avant de la ramener vers le Smoker à la vitesse des
|
||||
tyroliennes. Les obstacles sur le corps arrêtent le trajet ; blesser la source
|
||||
ou couper le crochet libère la victime. Tenue gris clair et vert olive, œuf
|
||||
créatif et familier adulte miniature à taille variable, avec son propre lasso.
|
||||
Build, **19 tests Smoker et 77 régressions** en sessions séparées, ainsi que
|
||||
le parcours **Vulkan FR/EN GUI 2/3/4**, réussis. Esquive au clavier, touche
|
||||
réelle, traction imposée du joueur et arrivée vérifiées.
|
||||
[Pack local beta.285](build/Sanctuary-beta.285.mrpack).
|
||||
|
||||
## beta.284 — Boomer et bile
|
||||
|
||||
[Quatrième infecté](docs/boomer-beta284.md) : gerbe de bile en cône,
|
||||
diversion temporaire des zombies déjà présents et éclaboussure à la mort,
|
||||
sans destruction du terrain. Tenue violette, pantalon sable, œuf créatif et
|
||||
familier adulte miniature à taille variable, avec une gerbe qui affaiblit
|
||||
les ennemis et une éclaboussure G qui garde le familier vivant.
|
||||
Build, **68 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4**
|
||||
réussis, avec G, recrutement sauvage, mort et trois tailles vérifiés.
|
||||
[Pack local beta.284](build/Sanctuary-beta.284.mrpack).
|
||||
|
||||
## beta.283 — Spitter et flaque d’acide
|
||||
|
||||
[Troisième zombie spécial](docs/spitter-beta283.md) : le crachat dépose une
|
||||
zone verte persistante au sol, comme une potion persistante. Les dégâts cessent
|
||||
en sortant ; aucun poison ne suit la victime. Tenue orange, pantalon bleu
|
||||
ardoise, œuf créatif et familier adulte miniature à taille variable.
|
||||
Build, **54 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4**
|
||||
réussis, avec G, dégâts dans la zone, sortie et trois tailles vérifiés.
|
||||
[Pack local beta.283](build/Sanctuary-beta.283.mrpack).
|
||||
|
||||
## beta.282 — Chargeur
|
||||
|
||||
[Deuxième zombie spécial](docs/charger-beta282.md) : préparation visible, charge
|
||||
en ligne droite, choc et récupération. Tenue verte, pantalon anthracite, œuf
|
||||
créatif et familier adulte miniature à taille variable, avec sa propre charge.
|
||||
Build, **45 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis,
|
||||
avec appui réel sur G, choc et recharge vérifiés.
|
||||
[Pack local beta.282](build/Sanctuary-beta.282.mrpack).
|
||||
|
||||
## beta.281 — Infectés adultes en miniature
|
||||
|
||||
[Les infectés abandonnent la forme bébé](docs/infected-adults-beta281.md).
|
||||
Le familier reprend les proportions adultes en miniature, avec son trait de
|
||||
taille variable conservé. La règle est commune aux futures variantes d'infectés.
|
||||
Build, **34 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis,
|
||||
avec trois tailles réelles de miniature vérifiées.
|
||||
[Pack local beta.281](build/Sanctuary-beta.281.mrpack).
|
||||
|
||||
## beta.280 — Premier infecté
|
||||
|
||||
[Un nouveau zombie et son familier](docs/infected-beta280.md) : modèle vanilla,
|
||||
t-shirt rouge, pantalon brun, 10 PV et vitesse légèrement supérieure au zombie.
|
||||
Œuf créatif `sanctuary:infected_spawn_egg`, apparitions peu fréquentes dans les
|
||||
habitats de zombies de l'Overworld et familier bébé avec son profil de combat.
|
||||
Texture vanilla recolorée pixel par pixel, sans déplacement UV. Build,
|
||||
**33 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis.
|
||||
[Pack local beta.280](build/Sanctuary-beta.280.mrpack).
|
||||
|
||||
## beta.279 — Carte des îles et grand dézoom
|
||||
|
||||
[Vue d'ensemble de l'archipel et relevés prolongés](docs/map-islands-zoom-beta279.md).
|
||||
La carte dézoome jusqu'à l'échelle du monde avec des niveaux de détail adaptés,
|
||||
et reçoit les nouveaux chunks lointains en gardant son centre fixe. Capacité
|
||||
portée de 16 384 à **131 072 chunks par couche et habitant**, sans effacer les
|
||||
relevés existants. Les propriétés et Demeure retrouvent leurs détails en zoomant.
|
||||
Build, **20 tests serveur ciblés** et parcours **Vulkan FR/EN GUI 2/3/4** réussis.
|
||||
[Pack local beta.279](build/Sanctuary-beta.279.mrpack).
|
||||
|
||||
## beta.278 — Disputer une propriété dans Battle
|
||||
|
||||
[Duel consenti pour un claim déjà revendiqué](docs/claim-battle-beta278.md).
|
||||
|
||||
+166
-2
@@ -1,5 +1,40 @@
|
||||
# Provenance et crédits
|
||||
|
||||
## Mods facultatifs beta.292
|
||||
|
||||
Les binaires officiels de [Simple Voice Chat 2.6.24+26.3](https://modrinth.com/plugin/simple-voice-chat/version/OLnMVWXy)
|
||||
(Max Henkel, All Rights Reserved), [Bobby 5.2.16+mc26.3](https://modrinth.com/mod/bobby/version/J7YnfCss)
|
||||
(johni0702, LGPL-3.0-or-later), [Lithium 0.26.2+mc26.3](https://modrinth.com/mod/lithium/version/xS0Q8LSi)
|
||||
(CaffeineMC et contributeurs, LGPL-3.0-only) et
|
||||
[Entity Culling 1.11.3 pour 26.3](https://modrinth.com/mod/entityculling/version/X1g3VhTx)
|
||||
(tr7zw et contributeurs, tr7zw Protective License) sont téléchargés séparément
|
||||
par packwiz, sans modification ni imbrication dans Sanctuary. Leurs notices,
|
||||
licences et bibliothèques imbriquées restent attachées aux JAR originaux.
|
||||
Leurs métadonnées et SHA-512 sont dans `packwiz/mods/` ;
|
||||
[choix de versions et vérifications](docs/optional-mods-beta292.md).
|
||||
|
||||
## Installateur packwiz Sanctuary beta.291
|
||||
|
||||
Le bandeau `launcher/packwiz/sanctuary.png` est le fichier `minecraft.png`
|
||||
fourni par le créateur le 26 octobre 2026, conservé sans modification.
|
||||
`InstallWindow.java` adapte la fenêtre de packwiz-installer v0.5.14 de
|
||||
comp500, sous MIT ; licence et source sont joints au JAR généré. Toutes les
|
||||
autres classes et les notices des dépendances de l'installateur officiel
|
||||
restent identiques. Sources, hashes et périmètre dans
|
||||
[le contrat du logo packwiz](docs/packwiz-logo-beta291.md).
|
||||
|
||||
## Tenues des zombies beta.287
|
||||
|
||||
Les neuf PNG `zombie.png`, `zombie2.png` à `zombie9.png` sont fournis par le
|
||||
créateur le 26 octobre 2026. Ils sont copiés sans modification sous
|
||||
`assets/sanctuary/textures/entity/zombies/zombie1.png` à `zombie9.png`.
|
||||
Le [reçu des sources et dérivés](docs/zombie-skins-beta287.json) conserve
|
||||
leurs SHA-256. `scripts/prepare_zombie_skins.py` produit aussi neuf adaptations
|
||||
UV pour le modèle bébé natif de Minecraft 26.3 par rééchantillonnage déterministe
|
||||
des faces ; aucun générateur d’image n’intervient. La géométrie et les couches
|
||||
d’armure restent issues de Minecraft. Les notices historiques des anciennes
|
||||
recolorations ci-dessous décrivent les livraisons précédentes.
|
||||
|
||||
Depuis beta.256, le pack client inclut [Sodium 0.9.2+mc26.3](https://modrinth.com/mod/sodium/version/bAZQdGpg),
|
||||
de JellySquid et des contributeurs CaffeineMC, sous Polyform-Shield-1.0.0.
|
||||
Le JAR officiel est une dépendance séparée téléchargée par packwiz ; il n’est
|
||||
@@ -106,8 +141,43 @@ L’icône d’objet correspondante provient de `../26.2/sanctuary`
|
||||
restent la propriété de leurs auteurs et ne sont pas placées sous la licence
|
||||
du code Sanctuary. Il ne s’agit pas d’une cape officielle accordée au compte.
|
||||
|
||||
Les familiers utilisent les modèles et textures résolus par Minecraft installé ;
|
||||
aucune copie des textures de mobs ni aucun ancien pouvoir n’est embarqué.
|
||||
Les familiers vanilla utilisent les modèles et textures résolus par Minecraft
|
||||
installé. L'exception du premier infecté est décrite ci-dessous.
|
||||
|
||||
## Infecté beta.280
|
||||
|
||||
Le modèle, les animations, l'armure et les sons sont ceux du zombie installé
|
||||
avec Minecraft 26.3. Aucun modèle ou son de Left 4 Dead n'est inclus ; cette
|
||||
série reprend seulement l'idée de rôles différents pour des zombies.
|
||||
|
||||
La texture `assets/sanctuary/textures/entity/infected.png` est une recoloration
|
||||
déterministe du zombie **Minecraft 26.3**, réalisée par
|
||||
`scripts/generate_infected_texture.py` à partir du JAR installé. Seules les
|
||||
couleurs du t-shirt et du pantalon changent ; coordonnées UV, peau, visage,
|
||||
chaussures et transparence restent identiques. Empreinte SHA-256 du PNG natif :
|
||||
`1b88eea9b63889c2c3399773f887af375defc3aad0e7a375adc84249da54cbbd`.
|
||||
Aucune texture produite par ImageGen n'est livrée.
|
||||
|
||||
L'icône `textures/item/infected_spawn_egg.png` est reprise intacte de Sanctuary
|
||||
26.2, commit `26f2c2a0061619df1f146f9081d6740c560a7497`, module `itsalive`.
|
||||
Elle provenait d'une recoloration de l'œuf Minecraft par
|
||||
`itsalive/tools/generate_infected_textures.sh`. Ces ressources dérivées ne sont
|
||||
pas couvertes par la licence GPL du code ; les droits sur les ressources
|
||||
Minecraft restent à Mojang/Microsoft.
|
||||
|
||||
## Chargeur beta.282
|
||||
|
||||
`assets/sanctuary/textures/entity/charger.png` utilise le même PNG natif du
|
||||
zombie Minecraft 26.3 et les mêmes masques UV que l'Infecté. La sélection
|
||||
`--variant charger` de `scripts/generate_infected_texture.py` produit un
|
||||
t-shirt vert et un pantalon anthracite, avec peau, visage, chaussures et alpha
|
||||
conservés pixel pour pixel. Aucun dessin génératif ni modèle de Left 4 Dead.
|
||||
SHA-256 : `cf2344d870753ed621b9fd7c3fd7dfe0b3dc35294620e798f113b71abd27fbda`.
|
||||
|
||||
L'icône `textures/item/charger_spawn_egg.png` est reprise intacte du module
|
||||
`itsalive` de Sanctuary 26.2, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, générée à partir de l'œuf Minecraft.
|
||||
Les droits sur les ressources Minecraft restent à Mojang/Microsoft.
|
||||
|
||||
## JEI beta.024
|
||||
|
||||
@@ -214,3 +284,97 @@ leurs aperçus publics de haute qualité ; la cigale du soir utilise la
|
||||
conversion MP3 publique de Commons. Aucun enregistrement d’Animal Crossing
|
||||
n’est inclus. L’identification biologique du grillon reste non précisée ;
|
||||
la courtilière européenne sert d’analogue sonore.
|
||||
|
||||
## Spitter beta.283
|
||||
|
||||
`assets/sanctuary/textures/entity/spitter.png` est une recoloration déterministe
|
||||
du zombie Minecraft 26.3 via `scripts/generate_infected_texture.py --variant spitter`.
|
||||
Seuls le t-shirt orange et le pantalon bleu ardoise changent. Peau, visage,
|
||||
chaussures, alpha et UV restent identiques au PNG natif décrit ci-dessus.
|
||||
SHA-256 : `92f6a8f7ad09a817fc74bfc2728a5f75d34a838dcc91a2ddee336c31803c0592`.
|
||||
Aucun modèle, son ou dessin génératif de Left 4 Dead n'est inclus.
|
||||
|
||||
L'icône `textures/item/spitter_spawn_egg.png` est reprise intacte du module
|
||||
`itsalive` de Sanctuary 26.2, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, dérivée de l'œuf Minecraft.
|
||||
Les droits sur les ressources Minecraft restent à Mojang/Microsoft.
|
||||
|
||||
## Boomer beta.284
|
||||
|
||||
`assets/sanctuary/textures/entity/boomer.png` est une recoloration déterministe
|
||||
du zombie Minecraft 26.3 par `scripts/generate_infected_texture.py --variant boomer`.
|
||||
T-shirt violet et pantalon sable : 570 pixels de vêtements changent, avec
|
||||
visage, peau, chaussures, UV et transparence conservés. SHA-256 :
|
||||
`2cbc0a87435c8f12fbbd77bd3d7fc23c9843e9ae22493dbfb6ffe14ea492c230`.
|
||||
|
||||
Le même script recolore en violet la silhouette et les ombres de l'icône
|
||||
`spitter_spawn_egg.png` historique, décrite ci-dessus, pour produire
|
||||
`textures/item/boomer_spawn_egg.png`. SHA-256 :
|
||||
`dd11449f7072db2faf753e5d258acfe8e5d2c50116e076d9b799345ed5f62ffa`.
|
||||
Les droits sur ces ressources dérivées restent à Mojang/Microsoft. Aucun
|
||||
modèle, son ou bitmap de Left 4 Dead, ni texture générative, n'est livré.
|
||||
|
||||
## Smoker beta.285
|
||||
|
||||
`assets/sanctuary/textures/entity/smoker.png` est une recoloration déterministe
|
||||
du zombie Minecraft 26.3 par `scripts/generate_infected_texture.py --variant smoker`.
|
||||
T-shirt gris clair et pantalon vert olive : 570 pixels de vêtements modifiés ;
|
||||
visage, peau, chaussures, UV et alpha conservés. SHA-256 :
|
||||
`80b7b34c915466d2c022f2222bd5bedf5b5b6c39ab447a97dcae7edf66ba4d17`.
|
||||
|
||||
Le même script dérive `textures/item/smoker_spawn_egg.png` de la silhouette et
|
||||
des ombres de l'icône historique `spitter_spawn_egg.png`, avec la teinte du
|
||||
t-shirt. SHA-256 :
|
||||
`e73b7d1e08ef0f35bfc7bac3e81795b166b56d9a5267045bcef64ae311c256c6`.
|
||||
La tête du lasso utilise l'objet laisse natif et son câble le rendu natif des
|
||||
laisses tendues déjà employé par les tyroliennes. Aucun modèle, son ou bitmap
|
||||
de Left 4 Dead ni texture générative n'est inclus. Les droits sur les ressources
|
||||
Minecraft dérivées restent à Mojang/Microsoft.
|
||||
|
||||
## Hunter beta.286
|
||||
|
||||
`assets/sanctuary/textures/entity/hunter.png` est une recoloration déterministe
|
||||
du zombie Minecraft 26.3 par `scripts/generate_infected_texture.py --variant hunter`.
|
||||
T-shirt bleu et pantalon anthracite : 570 pixels de vêtements modifiés ; tête,
|
||||
peau, chaussures, UV et alpha conservés. SHA-256 :
|
||||
`1057efc7947105684a24fe165e83581b66fccc877687433cc00ed80231c2c970`.
|
||||
|
||||
Le même script dérive `textures/item/hunter_spawn_egg.png` de la silhouette et
|
||||
des ombres de l'icône historique `spitter_spawn_egg.png`, avec la teinte du
|
||||
t-shirt. SHA-256 :
|
||||
`294bbd145e09707f2006d51e9190b538b50148c44c13f3ecaf563e316f03384d`.
|
||||
Les sons et le modèle sont natifs à Minecraft. Aucun bitmap ou son de Left 4
|
||||
Dead ni texture générative n'est inclus. Les droits sur les ressources
|
||||
Minecraft dérivées restent à Mojang/Microsoft.
|
||||
|
||||
## Baleine fantôme volante beta.290
|
||||
|
||||
Le modèle, son animation, le rendu, l’IA et le spawner proviennent du module
|
||||
`itsalive` de Sanctuary 26.2, commit
|
||||
`94644a28f05b8010f06ec1181fe6c74a50ad80d6`, sous la licence du projet
|
||||
GPL-3.0-or-later. Cette source est elle-même le port de la baleine du JAR
|
||||
historique Real Ghost ; aucune archive 26.2 n’est redistribuée.
|
||||
`textures/entity/flying_whale/flying_whale.png` est repris octet pour octet.
|
||||
SHA-256 : `0828ba1b9443f31cdb0ce9c22393a8702a72b9e59c2a0c02f3dd6dedadd33fb8`.
|
||||
|
||||
L’icône `textures/item/flying_whale_spawn_egg.png` est une recoloration
|
||||
déterministe directe de `assets/minecraft/textures/item/egg.png` de Minecraft
|
||||
26.3, produite par `scripts/generate_whale_egg.py` ; silhouette, alpha et
|
||||
ombrage natifs conservés. Source SHA-256 :
|
||||
`3bfccf5fcfe66933d591e944f6b08456c8083f5c3898b924da52bfebc41e6322`.
|
||||
Résultat SHA-256 :
|
||||
`d476a3fedba3af3e463d6461b81cf6ca8702fe10ceb3934f426390d59da63f7e`.
|
||||
Les sons et particules sont natifs à Minecraft, référencés sans copie ; les
|
||||
droits sur les ressources Minecraft dérivées restent à Mojang/Microsoft.
|
||||
|
||||
|
||||
## Happy creepers
|
||||
|
||||
`assets/sanctuary/textures/entity/creeper/happy.png` est repris sans modification
|
||||
du module `itsalive` de Sanctuary 26.2, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, chemin
|
||||
`itsalive/src/main/resources/assets/itsalive/textures/entity/creeper/happy.png`.
|
||||
SHA-256 : `40e22bf0610cfc7ae8ca910b1e6bdce0c759ccbf8714df7566ebe5538cac24f0`.
|
||||
Il s'agit du visage souriant historique sur le modèle natif du creeper. Les
|
||||
droits sur les ressources Minecraft dérivées restent à Mojang/Microsoft.
|
||||
Aucune autre variante de texture ni ressource Mobopia n'est importée.
|
||||
|
||||
+20
-1
@@ -27,6 +27,25 @@ tasks.register('verifyWebDistribution', Exec) {
|
||||
}
|
||||
tasks.named('check') { dependsOn('verifyWebDistribution') }
|
||||
|
||||
tasks.register('verifyOptionalMods', Exec) {
|
||||
group = 'verification'
|
||||
commandLine('python3', 'scripts/verify_optional_mods.py')
|
||||
}
|
||||
tasks.named('check') { dependsOn('verifyOptionalMods') }
|
||||
|
||||
tasks.register('assemblePackwizInstaller', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Build and verify the Sanctuary-branded packwiz window.'
|
||||
commandLine('python3', 'scripts/packwiz_installer.py')
|
||||
}
|
||||
|
||||
tasks.register('verifyPackwizInstaller', Exec) {
|
||||
group = 'verification'
|
||||
dependsOn('assemblePackwizInstaller')
|
||||
commandLine('python3', 'scripts/test_packwiz_installer.py')
|
||||
}
|
||||
tasks.named('check') { dependsOn('verifyPackwizInstaller') }
|
||||
|
||||
tasks.register('verifyPack', Exec) {
|
||||
group = 'verification'
|
||||
description = 'Verify pinned pack dependencies, versions and index hashes.'
|
||||
@@ -36,7 +55,7 @@ tasks.register('verifyPack', Exec) {
|
||||
tasks.register('assemblePack', Exec) {
|
||||
group = 'distribution'
|
||||
description = 'Assemble a local packwiz pack including the built Sanctuary mod.'
|
||||
dependsOn(':sanctuary:build', ':jei:build', 'verifyPack', 'assembleResourcePack', 'assembleResourceTemplate')
|
||||
dependsOn(':sanctuary:build', ':jei:build', 'verifyPack', 'verifyOptionalMods', 'assembleResourcePack', 'assembleResourceTemplate', 'assemblePackwizInstaller')
|
||||
commandLine('python3', 'scripts/pack.py', 'assemble')
|
||||
}
|
||||
|
||||
|
||||
@@ -1958,6 +1958,12 @@ et le client graphique avec connexion réelle passent. Voir
|
||||
|
||||
## Réserve de thèmes futurs
|
||||
|
||||
Le cadrage du 26 octobre 2026 détaille les
|
||||
[tickets d'intégration du pack et des modules personnels](integrations-mods-next.md) :
|
||||
Simple Voice Chat, Bobby facultatif, Lithium, Entity Culling, Dozei Stream
|
||||
et Only Fun. Les quatre intégrations du pack sont préparées localement dans
|
||||
[beta.292](optional-mods-beta292.md) ; les deux modules personnels restent à cadrer.
|
||||
|
||||
Ces thèmes servent à retrouver la vision, pas à demander leur implémentation immédiate. On en extrait un ticket seulement lorsqu'il devient utile au prochain incrément jouable.
|
||||
|
||||
| Thème | Contenus à découper plus tard |
|
||||
|
||||
@@ -24,8 +24,12 @@ standard. Le MRpack beta.280 reste accessible par son ancienne adresse pour
|
||||
compatibilité des liens, sans devenir un deuxième bouton du site. Le parcours
|
||||
manuel de configuration et son lien dans le pied de page sont retirés.
|
||||
|
||||
L'archive légère contient uniquement `instance.cfg`, `mmc-pack.json`, l'icône et
|
||||
`minecraft/packwiz-installer-bootstrap.jar`. Elle démarre avec zéro mod local.
|
||||
L'archive contient uniquement `instance.cfg`, `mmc-pack.json`, l'icône,
|
||||
`minecraft/packwiz-installer-bootstrap.jar` et, à partir de la préparation
|
||||
beta.291, `minecraft/packwiz-installer.jar` avec le
|
||||
[logo Sanctuary embarqué](packwiz-logo-beta291.md). Elle démarre avec zéro mod
|
||||
local. L'installateur est vérifié par SHA-256 et conservé par
|
||||
`--bootstrap-no-update` ; la mise à jour automatique des mods reste active.
|
||||
L'instance est nommée **Sanctuary Beta**, avec `ManagedPack=false`,
|
||||
`OverrideCommands=true`, la sélection automatique de Java et le canal stable :
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# ZOMBIES-284 — Boomer et bile
|
||||
|
||||
Branche `codex/boomer`, sur le socle local beta.283 conservé. État : implémenté et vérifié localement en beta.284.
|
||||
|
||||
## Contrat
|
||||
|
||||
Quatrième infecté : modèle adulte du zombie vanilla 26.3, t-shirt violet et
|
||||
pantalon sable recolorés pixel par pixel, visage/peau/UV/alpha conservés.
|
||||
Identifiants `sanctuary:boomer` et `sanctuary:boomer_spawn_egg`.
|
||||
Aucun bébé, y compris via œuf ou chargement ; pas de conversion en noyé.
|
||||
|
||||
- Sauvage : 10 PV, vitesse 0,20, mêlée 2, poursuite 32 blocs. À courte distance,
|
||||
prépare une gerbe pendant 20 ticks, puis vomit dans un cône de 5 blocs,
|
||||
orienté au début de la préparation. Recharge de 120 ticks. La bile révèle
|
||||
pendant 6 secondes et affaiblit pendant 3 secondes ; elle ne crée pas d'acide.
|
||||
- Une cible enduite attire jusqu'à six zombies déjà présents à 12 blocs, avec
|
||||
ligne de vue et chunks chargés, pendant six secondes au maximum. Pas de
|
||||
nouvelles créatures, de chargement distant ou d'horde générée. La diversion
|
||||
expire proprement et conserve la cible précédente si elle est encore valide.
|
||||
Retirer la révélation arrête aussi la diversion ; maximum 64 zombies détournés
|
||||
simultanément par serveur. Les projections touchent au plus 12 victimes.
|
||||
- À sa mort, éclaboussure unique de 3 blocs, 2 dégâts et bile, sans destruction
|
||||
de blocs ni feu. Une suppression administrative n'active pas l'éclaboussure.
|
||||
Les infectés de la famille et les joueurs créatifs/spectateurs sont protégés.
|
||||
Les murs et la différence de hauteur bloquent les projections.
|
||||
- Œuf créatif et apparition rare, poids 4 et groupe d'un dans les habitats
|
||||
de zombies de l'Overworld.
|
||||
- Familier rare, adulte miniature à taille variable persistante, 14 PV et
|
||||
vitesse 0,25. Gerbe automatique en cône et G d'éclaboussure à 3 blocs :
|
||||
dégâts, faiblesse et révélation passent par les règles communes des familiers,
|
||||
avec protection du propriétaire/alliés/duels. Le familier reste vivant et
|
||||
ne détourne pas de monstres contre des tiers.
|
||||
- Recharge du Boomer conservée au chargement ; préparation interrompue annulée.
|
||||
Les diversions sont transitoires ; aucun format existant ne change.
|
||||
|
||||
## Vérification
|
||||
|
||||
- Suite serveur ciblée : **68 tests réussis**, dont **14 nouveaux**. Fixtures
|
||||
plates explicites, graine de test `0`, aucun monde personnel ouvert.
|
||||
- Couverture Boomer : œuf/profil/spawn, règle adulte, préparation et vraie gerbe,
|
||||
esquive latérale, mur et étage, recharge après interruption et sur le tick
|
||||
exact du tir, sauvegarde/rechargement, mort native unique, suppression sans
|
||||
éclaboussure, famille protégée, six zombies maximum sans nouvelle entité,
|
||||
navigation/attaque native et restauration de la cible précédente après délai,
|
||||
absence de chargement distant, G et cône du familier avec protections communes,
|
||||
recharge partagée et tailles 120/500/1000/1500/3000/6000 conservant l'individu.
|
||||
- Parcours client natif **Vulkan / MoltenVK 1.4.2 / Apple M1**, Minecraft 26.3,
|
||||
Java 25, Loader 0.19.5, monde de test neuf de graine `284` : réussi avec arrêt
|
||||
normal du client. Appui réel sur G, dégâts/faiblesse/révélation, recharge et
|
||||
survie du familier ; gerbe sauvage, recrutement d'un zombie existant et
|
||||
éclaboussure après vrai dégât mortel. Trois tailles réelles 500/1000/1500,
|
||||
renderer adulte, œuf, infobulles et écran FR/EN aux échelles GUI 2/3/4 vérifiés.
|
||||
- La première vérification générale a rencontré le test préexistant
|
||||
`Companion019GameTests.creatureProfilesActuallyMove` (déplacement aérien).
|
||||
Les contrôles Boomer passaient ; la suite de 68 a ensuite repassé avec ce
|
||||
test inchangé. Journal conservé dans
|
||||
`build/boomer284-check-build-pack-attempt1.log`.
|
||||
|
||||
- `./gradlew check build assemblePack` réussi en **2 min 40 s**, avec
|
||||
`-PsanctuaryFocusedTests=boomer284,spitter283,charger282,infected280,familiar260,companions`
|
||||
et `-PsanctuaryAtlasOnly=true`. La suite complète ciblée repasse ses 68 tests.
|
||||
- PNG du Boomer comparé en RGBA à la recoloration attendue du zombie natif
|
||||
26.3 épinglé : **331 pixels de t-shirt et 239 de pantalon** modifiés,
|
||||
tête, peau, chaussures, alpha et coordonnées UV conservés. Les textures des
|
||||
trois infectés précédents restent identiques. L'œuf est une recoloration
|
||||
violette de la silhouette et des ombres de l'icône historique, sans changement
|
||||
d'alpha ; aucun outil de génération d'images utilisé.
|
||||
- JAR et export MRpack : ZIP intègres, version beta.284 et dépendances
|
||||
Minecraft 26.3 / Loader 0.19.5 contrôlées. Classes, œuf, texture, profil,
|
||||
loot, traductions et tags Boomer présents ; classes de tests exclues.
|
||||
Le JAR exporté est identique au JAR compilé. Pack beta.283 précédent conservé
|
||||
octet pour octet.
|
||||
|
||||
[Pack local beta.284](../build/Sanctuary-beta.284.mrpack), **16 650 758 octets**.
|
||||
SHA-256 : `4a27ce2b184c8797f0ed7891c6dc62d1db372e2fdabb4fd5de98389df16f25a3`.
|
||||
[Reçu et empreintes](../build/boomer284-validation.json).
|
||||
|
||||

|
||||
|
||||
La livraison reste locale : aucun tag/release, canal packwiz, serveur personnel
|
||||
ou instance Prism modifié. Les œufs restent créatifs ; leur acquisition en
|
||||
survie et les autres infectés demandent leurs propres tickets. Aucune génération
|
||||
de terrain ni sauvegarde personnelle modifiée.
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# ZOMBIES-282 — Chargeur
|
||||
|
||||
Branche `codex/charger`, sur le socle local beta.281 conservé. État : livré et vérifié localement.
|
||||
|
||||
## Contrat
|
||||
|
||||
- `sanctuary:charger`, œuf `sanctuary:charger_spawn_egg`, noms FR/EN. Modèle
|
||||
zombie adulte uniquement, tenue verte et pantalon anthracite ; texture native
|
||||
26.3 recolorée par script, visage, peau, chaussures et UV conservés.
|
||||
- Attributs de base : 10 PV, vitesse 0,27, mêlée 3, poursuite 36 blocs.
|
||||
Charge depuis 4 à 15 blocs avec ligne de vue et sol sous les pieds :
|
||||
préparation visible de 22 ticks, direction figée au départ, course de
|
||||
24 ticks au maximum, choc de 4 dégâts avec recul, récupération de 48 ticks,
|
||||
puis recharge de 100 ticks. Mêlée suspendue pendant ces trois phases.
|
||||
- La collision est celle du déplacement réel : pas de frappe à distance ou
|
||||
à travers un mur. Un joueur peut esquiver en sortant de la trajectoire.
|
||||
Obstacles, cible invalide ou bord d'une zone chargée terminent la charge.
|
||||
Aucune destruction de bloc, aucune téléportation ou génération forcée.
|
||||
- Spawn peu fréquent dans les habitats de zombies de l'Overworld, poids 4,
|
||||
un seul individu. Soleil, statut mort-vivant et absence de conversion en
|
||||
noyé comme l'Infecté. Les autres variantes restent à réaliser.
|
||||
- Familier adulte miniature, taille persistante variable, profil rare dédié :
|
||||
16 PV, vitesse 0,28, mêlée ; technique G de charge droite jusqu'à 8 blocs,
|
||||
préparation 22 ticks, récupération 24 ticks, recharge 12 secondes, dégâts 4.
|
||||
Variante apprise : ruée droite courte de 5 blocs. Droits, alliés, limites de
|
||||
dégâts et de contrôle restent ceux des familiers existants. Une charge
|
||||
déclenchée consomme sa recharge même si elle manque sa cible.
|
||||
- Œuf disponible en créatif ; pas de nouvelle acquisition en survie.
|
||||
|
||||
## Référence et sauvegardes
|
||||
|
||||
Référence en lecture seule : `../26.2/itsalive`, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, variante `CHARGER` et
|
||||
`tickCharger`. Les phases de l'ancien comportement servent de référence ; le
|
||||
contrôle des coups repose ici sur la trajectoire réellement parcourue.
|
||||
|
||||
Le nouveau type a sa propre persistance native. Pour la charge, seule la recharge est sauvée ;
|
||||
une phase interrompue reprend au repos avec une recharge à la relecture.
|
||||
Les identifiants, formats, individus, œufs et textures déjà livrés restent
|
||||
stables. Aucun monde personnel ouvert ; aucun chunk ou terrain réécrit.
|
||||
|
||||
## Validation
|
||||
|
||||
Les essais serveur ciblés ont validé les phases du mob avec déplacement natif,
|
||||
le choc et son recul, la récupération sans double mêlée, l'esquive latérale,
|
||||
un mur ajouté après le départ, la persistance d'une charge interrompue, le
|
||||
profil et l'œuf, les dégâts du familier, sa trajectoire et sa recharge sur
|
||||
échec. Deux contrôles couvrent aussi une téléportation interrompant la course
|
||||
et les commandes d'une monture pendant la préparation. La sonde du corridor
|
||||
complet vérifie les chunks avant toute collision ; elle ne les charge pas.
|
||||
La construction finale **`check build assemblePack` passe en 2 min 59 s**,
|
||||
avec les **45 cas ciblés**, dont onze nouveaux pour le Chargeur, et les contrôles
|
||||
purs du dépôt. Log : `build/charger282-check-build-pack.log`.
|
||||
|
||||
Le parcours client **Vulkan / MoltenVK 1.4.2 / Apple M1** passe en **1 min 03 s** :
|
||||
vrai appui sur G, déplacement et choc du familier, recharge et récupération,
|
||||
phases préparation/course/choc d'un Chargeur sauvage, géométrie adulte,
|
||||
tailles 500/1 000/1 500 ‰, rareté, infobulles et menus **FR/EN GUI 2/3/4**.
|
||||
Douze captures sont conservées sous `build/charger282-screenshots/` ; vues de
|
||||
la miniature ordinaire, de la préparation et du menu FR GUI 2 inspectées.
|
||||
Log : `build/charger282-client-vulkan.log`, marqueur `CHARGER282_CLIENT_PASS`.
|
||||
|
||||
La comparaison des PNG confirme **570 pixels de vêtements recolorés**, avec
|
||||
558 pixels de peau/visage et tous les alpha intacts. La texture de l'Infecté
|
||||
rouge conserve exactement son empreinte beta.281. Génération reproductible :
|
||||
|
||||
```sh
|
||||
python3 scripts/generate_infected_texture.py /chemin/minecraft-26.3-client.jar --variant charger
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=charger282,infected280,familiar260,companions \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryCharger282ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Un contrôle historique de déplacement du dragon a échoué sur un passage
|
||||
intermédiaire, puis passé sans changement sur le suivant ; sa stabilité n'est
|
||||
pas démontrée par ce ticket. Un premier parcours client a passé ses étapes
|
||||
avant une erreur fastutil dans `ChunkMap` à la fermeture du serveur de test.
|
||||
La fixture nettoie désormais ses acteurs et attend leur retrait ; le parcours
|
||||
final se termine normalement.
|
||||
|
||||
## Essai local
|
||||
|
||||
```mcfunction
|
||||
/give @s sanctuary:charger_spawn_egg
|
||||
/summon sanctuary:charger
|
||||
```
|
||||
|
||||
L'œuf équipé invoque le familier miniature. G lance la charge ; les ordres et
|
||||
la sélection d'aptitudes suivent les contrôles habituels. Pendant une charge
|
||||
montée, préparation et récupération comprises, le pilote ne peut pas détourner
|
||||
la trajectoire avec les commandes de déplacement. Les autres profils utilisant
|
||||
`CHARGE` gardent leur comportement ; seul l'effet de profil `straight` utilise
|
||||
ce nouveau contrôleur.
|
||||
|
||||
La fréquence des rencontres naturelles en partie longue et le comportement
|
||||
sur toutes les formes de relief ne sont pas mesurés. Aucun monde personnel,
|
||||
serveur, canal packwiz ou instance Prism modifié.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- Mod : `mods/sanctuary/build/libs/sanctuary-beta.282.jar`.
|
||||
- Pack : `build/Sanctuary-beta.282.mrpack`.
|
||||
- Captures : `build/charger282-screenshots/`.
|
||||
- Reçu : `build/charger282-validation.json`.
|
||||
|
||||
JAR et MRpack vérifiés : version exacte, intégrité ZIP, Minecraft 26.3, Fabric
|
||||
Loader 0.19.5, un seul JAR Sanctuary identique au build, profils et contrôleurs
|
||||
de charge présents, deux textures identiques aux sources et fixtures exclues.
|
||||
@@ -0,0 +1,53 @@
|
||||
# PACK-03 — Serveur Sanctuary préenregistré et icône
|
||||
|
||||
Ticket du **26 octobre 2026**, branche `codex/default-server`, version `beta.295`.
|
||||
|
||||
Le premier démarrage du client ajoute **Sanctuary Beta**, adresse publique
|
||||
`beta.sanctuary-minecraft.net`, dans la liste multijoueur vanilla. Le mécanisme
|
||||
est livré dans le JAR : il fonctionne après import du ZIP Prism du site,
|
||||
installation MRpack et mise à jour d'une instance packwiz existante.
|
||||
|
||||
`servers.dat` reste un fichier personnel, jamais géré par packwiz. L'ajout
|
||||
conserve l'ordre, les noms, réglages et champs inconnus des autres favoris.
|
||||
Un favori Sanctuary existant est conservé sans doublon, y compris avec le nom
|
||||
DNS en majuscules, un point final ou les ports 25565/25567. Une entrée cachée
|
||||
issue de connexion directe devient visible sans être renommée. Avant la
|
||||
première écriture, une copie exacte est conservée dans
|
||||
`servers.dat.before-sanctuary-beta`. Un fichier illisible reste intact.
|
||||
|
||||
Le marqueur local `.sanctuary/default-server-beta` indique que l'ajout initial
|
||||
est terminé : le joueur peut ensuite renommer, déplacer ou supprimer le favori.
|
||||
Il n'est pas recréé à chaque lancement. Aucun téléchargement ni connexion de
|
||||
jeu automatique n'est ajouté.
|
||||
|
||||
Le logo 160 × 160 du pack est redimensionné en PNG 64 × 64, sans nouvelle
|
||||
illustration. Cette même image sert d'icône initiale du favori et de
|
||||
`server-icon.png` dans le profil serveur packwiz. Le serveur annonce ainsi
|
||||
l'icône aux clients lors du ping de la liste multijoueur. Le profil client ne
|
||||
possède jamais `servers.dat` ; le profil serveur possède explicitement cette
|
||||
icône. Les mondes et réglages de jeu ne changent pas.
|
||||
|
||||
La livraison commune intègre le ticket terminé
|
||||
[CLAIM-03](property-command-beta294.md) : seule la commande anglaise
|
||||
`/sanctuary property` et ses sous-commandes sont conservées. Les deux fichiers
|
||||
de production, les deux fichiers de GameTests et le contrat sont repris
|
||||
exactement depuis sa préparation locale beta.294, après ses 18 tests serveur
|
||||
réussis, sans modifier son checkout de travail.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- `defaultServerListSmoke` : 15 contrôles, dont lecture par `ServerData`
|
||||
vanilla, icône 64 × 64, favoris existants, sauvegarde exacte, suppression,
|
||||
absence de doublon et fichier corrompu suivi d'une réparation.
|
||||
- Parcours client **Vulkan FR/EN**, GUI 3 en 1280 × 800 : réussi en 38 s,
|
||||
hook réel de démarrage, liste vanilla, nom et icône, ping public 0/20.
|
||||
Deux captures inspectées ; aucun monde ouvert. Journal et captures sous
|
||||
`build/release295/` et `mods/sanctuary/build/run/clientGameTest/screenshots/`.
|
||||
- `./gradlew check build assemblePack assembleServerPack` : réussi en
|
||||
**3 min 22 s**, avec les contrôles purs et **18 tests serveur ciblés**
|
||||
`property270,claims272,claims277,claims278`, `sanctuaryAtlasOnly=true`.
|
||||
Les tests vérifient à nouveau la commande unifiée sur la base commune.
|
||||
Journal : `build/release295/check-build.log`.
|
||||
- **Publication effectuée** : Gitea, canal client/serveur, CT 350, téléchargement
|
||||
Web et instance Prism historique en beta.295. [Reçu complet](release-beta295.md),
|
||||
sauvegardes et limite de l’instance importée encore ouverte.
|
||||
@@ -0,0 +1,88 @@
|
||||
# LIGHT-288 — intégrer la correction des lumières portées
|
||||
|
||||
Branche `codex/dynamic-light-integration`, socle local beta.287 conservé.
|
||||
État : intégré, vérifié et construit en pack local beta.288.
|
||||
|
||||
## Résultat demandé
|
||||
|
||||
Intégrer au pack courant le correctif du commit
|
||||
`6f44214f2725b4ef2d1002606cb83e70885186ec`, branche `codex/dynamic-hand-light`.
|
||||
Sa version locale isolée était beta.283 ; la livraison combinée prend beta.288
|
||||
et conserve les zombies, leurs neuf tenues partagées, les bras natifs et le
|
||||
Hunter accroupi de beta.287.
|
||||
|
||||
Une torche ou une lanterne tenue réagissait dans les normales PBR sans éclairer
|
||||
le terrain avec Sodium. La collecte des sources était correcte, mais les copies
|
||||
`LevelSlice` utilisées par les workers Sodium ne recevaient pas l’instantané.
|
||||
|
||||
L’interface `DynamicLightContext` et deux mixins optionnels transmettent
|
||||
l’instantané immuable du contexte préparé côté rendu vers la copie du worker.
|
||||
Chaque nouveau travail remplace sa capture et `reset` la libère. Les lectures
|
||||
directes de lumière de bloc dans Sodium utilisent le même champ radial que
|
||||
le chemin natif, sans modifier le canal du ciel.
|
||||
|
||||
Les mixins ne sont sélectionnés que côté client avec Sodium présent. La
|
||||
version exacte reste `0.9.2+mc26.3`, facultative et active par défaut dans le
|
||||
pack ; aucune nouvelle dépendance. Les budgets, réglages et chemins natifs
|
||||
existants restent en vigueur. Aucun monde, identifiant, format de sauvegarde
|
||||
ou comportement serveur n’est modifié.
|
||||
|
||||
## Vérification
|
||||
|
||||
Le test existant `HandLight283ClientChecks` est repris sans modification du
|
||||
commit source. Monde plat neuf, graine `283`, salle en pierre fermée et zone
|
||||
de pixels du sol sans interface ni objet en main visible. Deux parcours
|
||||
**Vulkan / MoltenVK 1.4.2, Apple M1** réussis :
|
||||
|
||||
| Rendu du pack combiné | Sol sans source | Sol avec torche |
|
||||
| --- | ---: | ---: |
|
||||
| Sodium `0.9.2+mc26.3` | 11,785 | 85,366 |
|
||||
| Natif, Sodium absent | 11,783 | 90,043 |
|
||||
|
||||
Ces moyennes de luminance sur 255 sont des mesures de pixels, pas des niveaux
|
||||
de lumière du serveur. Les deux mains, l’AO activée/désactivée, shaders/PBR
|
||||
coupés, profil Moyen sans/avec PBR, déplacement et torche jetée sont vérifiés.
|
||||
Après retrait, le sol retrouve sa luminosité initiale ; le moteur de lumière
|
||||
du monde reste à zéro. Avec Sodium, le worker retardé conserve son instantané,
|
||||
la réutilisation le remplace, le ciel reste inchangé et `reset` le libère.
|
||||
|
||||
Commandes des deux parcours, en sessions distinctes :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryHandLightClientTests=true -PsanctuarySodiumClientTests=true \
|
||||
-PsanctuaryQuickTests=true -PsanctuaryClientGraphicsBackend=vulkan \
|
||||
-PsanctuaryClientHeapGb=8 -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le second parcours utilise `-PsanctuarySodiumClientTests=false`. Le parcours
|
||||
`Zombie287ClientChecks` réussit également avec Sodium activé, Vulkan, graine
|
||||
`287` : neuf tenues partagées pour les sept types, bébé classique, bras natifs,
|
||||
21 combinaisons de familiers et marche/bond du Hunter. Logs et captures sous
|
||||
`build/dynamic288/`.
|
||||
|
||||
`./gradlew check build assemblePack -PsanctuaryFocusedTests=lights,hunter286
|
||||
-PsanctuaryAtlasOnly=true` réussit en **2 min 42 s** : **19 GameTests requis**
|
||||
pour les lumières et le Hunter, test d’infrastructure compris, ainsi que tous
|
||||
les contrôles purs configurés par `check`.
|
||||
|
||||
L’export packwiz est vérifié : intégrité ZIP, version `beta.288`, Minecraft
|
||||
`26.3`, Fabric Loader `0.19.5`, un seul JAR Sanctuary identique au binaire
|
||||
construit. Par rapport au JAR beta.287, seules **cinq entrées** changent :
|
||||
la version Fabric, le plugin de sélection des mixins et les trois nouvelles
|
||||
classes du pont Sodium. Toutes les classes et ressources des zombies et
|
||||
familiers sont strictement identiques. Les tests et Sodium ne sont pas
|
||||
embarqués dans le JAR Sanctuary.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
- [Pack beta.288](../build/Sanctuary-beta.288.mrpack), 16 715 918 octets.
|
||||
SHA-256 : `51f70f5f00b0ccd293cf9be0dd34cf3e125c493d7929393428623668543c396b`.
|
||||
- JAR `mods/sanctuary/build/libs/sanctuary-beta.288.jar`.
|
||||
SHA-256 : `189883fad82476da221552fc56facb585f05f3167d0908a855f510625b932358`.
|
||||
- Reçu : `build/dynamic288/validation.json`, comparaison des archives et
|
||||
références des logs. Le MRpack beta.287 reste intact.
|
||||
|
||||
Aucune publication, synchronisation Prism ou ouverture de sauvegarde
|
||||
personnelle. La décroissance radiale et ses limites d’occultation restent
|
||||
celles de l’éclairage dynamique existant.
|
||||
@@ -0,0 +1,105 @@
|
||||
# WHALE-01 — baleine fantôme volante
|
||||
|
||||
Branche `codex/flying-ghost-whale`, préparation locale **beta.290** le
|
||||
26 octobre 2026. Demande : import direct de la baleine de Sanctuary 26.2,
|
||||
avec son IA et son œuf, deux baleines présentes au maximum sur le serveur.
|
||||
|
||||
## Import
|
||||
|
||||
Sources historiques : `26.2/itsalive`, commit
|
||||
`94644a28f05b8010f06ec1181fe6c74a50ad80d6`. Le modèle, ses animations,
|
||||
la texture nuageuse 512 × 256, les particules et les sons Minecraft sont
|
||||
repris. Les API Fabric/Minecraft sont adaptées à **Minecraft 26.3**, sans
|
||||
nouvelle dépendance. La texture de la baleine est copiée octet pour octet.
|
||||
|
||||
- `sanctuary:flying_whale` : baleine géante, paisible, volante, intangible,
|
||||
invulnérable, sans butin. Dimensions historiques 36 × 11, rendu ×2,7.
|
||||
- IA conservée : compagnon vivant non spectateur le plus proche à 520 blocs,
|
||||
cibles successives entre 68 et 140 blocs du joueur, altitude libre, virages
|
||||
progressifs, souffle et lente errance sans joueur.
|
||||
- Spawner historique : contrôle toutes les 400 ticks, une apparition réussie
|
||||
par tiers de journée (8 000 ticks), dans l’Overworld à 92–138 blocs d’un
|
||||
joueur. Il attend si le chunk candidat n’est pas déjà chargé. Aucun chunk
|
||||
n’est généré ou forcé par le spawner. Le calendrier appartient au serveur
|
||||
courant et repart au démarrage, comme le calendrier historique.
|
||||
- Dissipation au plus tard après 14 400 ticks (12 minutes à 20 TPS), possible
|
||||
dès 12 000 ticks après plus de 900 ticks sans joueur proche. Seules les
|
||||
baleines nommées sont sauvegardées, comme en 26.2 ; elles restent soumises
|
||||
à cette durée de vie après chargement.
|
||||
|
||||
Le quota historique de deux baleines chargées est partagé entre **toutes les
|
||||
dimensions du serveur**, et étendu aux œufs, invocations, distributeurs et
|
||||
rechargements. Une troisième entité chargée est retirée. L’utilisation
|
||||
manuelle d’un œuf est refusée avant consommation si les deux places sont
|
||||
prises, avec un message FR/EN. Retirer une baleine libère immédiatement sa
|
||||
place. Les baleines nommées dans des chunks déchargés ne réservent pas de
|
||||
place ; elles repassent par le quota au rechargement.
|
||||
|
||||
## Œuf Minecraft personnalisé
|
||||
|
||||
`sanctuary:flying_whale_spawn_egg` figure dans l’onglet Œufs d’apparition.
|
||||
Son icône est dérivée directement de **`assets/minecraft/textures/item/egg.png`
|
||||
de Minecraft 26.3**, sans reprendre le dessin historique de l’œuf.
|
||||
`scripts/generate_whale_egg.py` recolore les pixels en bleu nuage pâle avec
|
||||
huit pixels lavande. Dimensions 16 × 16, les 90 pixels de coquille,
|
||||
transparence, contour asymétrique et emplacements des ombres restent natifs.
|
||||
Le script refuse une version ou une empreinte source différente et propose
|
||||
`--check` pour comparer les pixels livrés sans réécriture.
|
||||
|
||||
```sh
|
||||
python3.14 scripts/generate_whale_egg.py --check /chemin/minecraft-client.jar
|
||||
/give @s sanctuary:flying_whale_spawn_egg
|
||||
/summon sanctuary:flying_whale ~ ~10 ~
|
||||
```
|
||||
|
||||
Libellés de l’entité, de l’œuf et du refus de quota en FR/EN. Aucun familier
|
||||
miniature, capture de baleine ou conversion d’ID 26.2 n’est ajouté. Pas de
|
||||
changement de générateur, de sauvegarde existante ou de publication distante.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `./gradlew check build assemblePack
|
||||
-PsanctuaryFocusedTests=whale290,recipes,progression
|
||||
-PsanctuaryAtlasOnly=true` : **réussi en 3 min 34 s**, avec **19 tests
|
||||
serveur requis réussis**, tous les contrôles purs de `check`, le JAR
|
||||
`sanctuary-beta.290.jar` et les profils packwiz client/serveur assemblés.
|
||||
Ce périmètre serveur cible la baleine, les recettes et la progression ;
|
||||
il ne rejoue pas les suites de génération historiques.
|
||||
- Archive JAR relue : version beta.290, classes et deux textures présentes,
|
||||
un seul JEI embarqué, aucune classe de test ; même JAR unique dans chacun
|
||||
des deux profils packwiz. Reçu SHA-256 sous `build/whale290/artifacts.json`.
|
||||
- Suite native ciblée `:sanctuary:runGameTest
|
||||
-PsanctuaryFocusedTests=whale290 -PsanctuaryAtlasOnly=true` : **7 tests
|
||||
requis réussis**, dont les six scénarios baleine et le contrôle
|
||||
d’infrastructure. Usage réel de l’œuf en survie, non-consommation à quota
|
||||
plein, quota Overworld/Nether, refus de la troisième invocation, libération
|
||||
d’une place, apparition naturelle à rayon/altitude historiques, déplacement
|
||||
natif, dissipation, collision sans modification du terrain et sauvegarde
|
||||
nommée vérifiés. Les tests attendent que les chunks de développement soient
|
||||
effectivement chargés avant de vérifier leur population.
|
||||
- Parcours graphique **Vulkan / MoltenVK 1.4.2 / Apple M1**, avec
|
||||
**Sodium 0.9.2+mc26.3**, réussi en **49 s** : deux baleines géantes
|
||||
translucides, nageoires animées, texture historique, véritable déplacement
|
||||
autour du joueur, œuf en main et hotbar, libellés FR/EN. Les captures
|
||||
`0000_whale290-native-egg-and-two-whales.png` et
|
||||
`0001_whale290-ghosts.png` sont inspectées visuellement.
|
||||
- `generate_whale_egg.py --check` : pixels livrés identiques au résultat
|
||||
déterministe, 90 pixels opaques et masque alpha strictement natif.
|
||||
PNG de la baleine identique octet pour octet à la référence 26.2 ;
|
||||
JSON des libellés FR/EN et modèles d’objet valides.
|
||||
|
||||
Commande du parcours graphique :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryWhale290ClientTests=true -PsanctuarySodiumClientTests=true \
|
||||
-PsanctuaryFocusedTests=whale290 -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Journaux et captures ignorés sous `build/whale290/`. Validation en monde
|
||||
plat de développement neuf, graine `290` pour le client ; aucun monde
|
||||
personnel ni serveur Beta n’est ouvert ou modifié. Le calendrier complet
|
||||
sur une journée réelle et un parcours multijoueur distant ne sont pas
|
||||
revendiqués ; les trois fenêtres sont exercées par les tests de calendrier.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Happy creepers — réintégration
|
||||
|
||||
Branche `codex/happy-creepers`, worktree isolé du chantier baleines.
|
||||
Version locale préparée : **beta.291**, sur le socle beta.289.
|
||||
|
||||
Cette fiche conserve le reçu de la branche dédiée. Son commit `0ffd371a` est
|
||||
repris dans la [livraison intégrée beta.293](release-beta293.md), avec les
|
||||
baleines, le logo packwiz et les quatre mods facultatifs. Consulter ce reçu
|
||||
pour l'état de publication et les vérifications de la combinaison finale.
|
||||
|
||||
## Contrat
|
||||
|
||||
Le happy creeper est un état du creeper Minecraft existant. **Maj + clic droit
|
||||
avec une fleur** équipe son unique emplacement cosmétique de tête ; **Maj +
|
||||
clic droit à main vide** rend cette même fleur. Le geste, les contrôles de
|
||||
distance/visibilité, la consommation en survie et la conservation des composants
|
||||
de l'objet sont ceux du système de chapeaux existant. Un emplacement occupé
|
||||
doit être vidé avant d'équiper une autre fleur.
|
||||
|
||||
Toute fleur du tag Minecraft `minecraft:flowers` rend le creeper heureux :
|
||||
visage souriant historique, fleur exacte visible sur la tête animée, aucun
|
||||
ciblage hostile ni explosion. L'équipement annule immédiatement une mèche
|
||||
déjà amorcée, y compris au briquet ; retirer la fleur ne reprend pas cette
|
||||
ancienne mèche. Tant que la fleur est portée, briquet et boule de feu ne
|
||||
l'allument pas et ne sont pas consommés ou usés. Le creeper continue d'utiliser
|
||||
ses déplacements natifs et peut toujours subir des dégâts.
|
||||
|
||||
Retirer la fleur ou la remplacer par un objet sans rapport rétablit le visage,
|
||||
le ciblage et l'explosion ordinaires. Un pot de fleurs, des feuilles ou des
|
||||
graines ne déclenchent pas cet état. La charge électrique est indépendante :
|
||||
un creeper chargé peut être heureux, et la foudre peut charger un happy
|
||||
creeper. Retirer sa fleur conserve sa charge.
|
||||
|
||||
## Sauvegarde et périmètre
|
||||
|
||||
L'état heureux se déduit uniquement de l'objet dans `sanctuary:mob_head`, dont
|
||||
le schéma 1 est réutilisé sans modification. Cette pile est déjà sauvegardée
|
||||
et synchronisée par les chapeaux de mobs. Aucun second drapeau, nouvelle
|
||||
entité, changement de format, migration de monde ou tirage de variante n'est
|
||||
ajouté. Un creeper portant déjà une fleur devient donc heureux avec cette
|
||||
version. L'identité, la santé, la charge et les autres données natives restent
|
||||
sur le même creeper. Le port d'un chapeau conserve sa protection habituelle
|
||||
contre la disparition à distance.
|
||||
|
||||
Les nouvelles apparences de creepers restent pour un prochain ticket. Le
|
||||
suivi du joueur, les apparitions naturelles de happy creepers et les anciennes
|
||||
entités `happy_creeper:*`/`mobopia:*` ne font pas partie de cet incrément.
|
||||
Aucune nouvelle interface n'est ajoutée ; les messages d'équipement et de
|
||||
retrait FR/EN existants sont réutilisés.
|
||||
|
||||
## Vérification
|
||||
|
||||
- Serveur `happycreeper,cosmetics,headblocks` : **28 tests requis réussis**,
|
||||
dont sept tests happy creeper et le test d'infrastructure Fabric. Annulation
|
||||
immédiate d'une vraie mèche à vingt ticks, refus répété du ciblage/allumage,
|
||||
représailles, tous les objets du tag fleurs, récupération de la pile nommée,
|
||||
allumage natif sans fleur et rechargement du même creeper chargé vérifiés.
|
||||
- Parcours client **Vulkan avec Sodium** : **réussi**. Vrais appuis sur Maj et
|
||||
clics souris pour équiper/récupérer une fleur en survie ; synchronisation de
|
||||
la pile et du visage ; AI active pendant 85 ticks près d'un vrai joueur ;
|
||||
sourire, fleur sur la tête animée et aura électrique native vérifiés.
|
||||
Monde plat de développement neuf, graine `290`, Minecraft 26.3 sur Apple M1.
|
||||
Capture : `build/happy-creepers-screenshots/four-states-vulkan.png` ; de gauche
|
||||
à droite : chargé, chargé heureux, heureux, ordinaire.
|
||||
- `check build assemblePack` : **réussi en 3 min 55 s**, contrôles purs et
|
||||
**28 tests serveur** inclus. Export MRpack réussi, versions et dépendances
|
||||
exactes contrôlées ; un seul JAR Sanctuary embarqué, identique au JAR
|
||||
construit ; texture historique identique octet par octet et classes de test
|
||||
absentes. Logs : `build/happy-creepers-final-check-build.log`,
|
||||
`build/happy-creepers-server.log`, `build/happy-creepers-vulkan.log`.
|
||||
Reçu : `build/happy-creepers-validation.json`.
|
||||
|
||||
Commandes reproductibles :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=happycreeper,cosmetics,headblocks \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryHappyCreeperClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuarySodiumClientTests=true \
|
||||
-PsanctuaryClientHeapGb=4 -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le premier essai de `check build` associait par erreur le preset unifié et le
|
||||
profil rapide ; il a échoué au choix du générateur avant les tests. Le profil
|
||||
plat `sanctuaryAtlasOnly=true` évite ce conflit de configuration existant.
|
||||
Aucun monde personnel, serveur Beta, canal packwiz ou Prism n'est modifié.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- [Pack beta.291](../build/Sanctuary-beta.291.mrpack), 16 724 142 octets.
|
||||
SHA-256 : `3c2d630fc85e88b08bac94cc2244e975d3552fcac4d508ad366adea730062468`.
|
||||
- JAR : `mods/sanctuary/build/libs/sanctuary-beta.291.jar`.
|
||||
SHA-256 : `e8e315db1e4831400244d17429723099fbfe3a352387d8a9cf0ecb01d8170e9e`.
|
||||
|
||||
Cette branche part de beta.289 et ne contient pas le travail parallèle des
|
||||
baleines préparé en beta.290. L'intégration des deux branches précède toute
|
||||
éventuelle publication commune. Aucun tag ni artefact distant n'est créé.
|
||||
@@ -0,0 +1,87 @@
|
||||
# ZOMBIES-286 — Hunter et bond offensif
|
||||
|
||||
Branche `codex/hunter`, sur le socle local beta.285 conservé. État : implémenté,
|
||||
vérifié côté serveur et sous Vulkan ; pack local beta.286 construit et vérifié.
|
||||
|
||||
## Contrat demandé
|
||||
|
||||
Sixième infecté : un Hunter qui prépare son bond, saute réellement vers sa
|
||||
cible et la blesse seulement si son corps la touche. Le bond peut rater : sa
|
||||
direction reste fixée après le départ et les blocs utilisent leurs collisions
|
||||
natives. Aucun plaquage ni dégât répété automatique n'est ajouté.
|
||||
|
||||
- Zombie adulte vanilla 26.3, t-shirt bleu soutenu et pantalon anthracite
|
||||
recolorés par script ; tête, peau, chaussures, UV et alpha conservés.
|
||||
Identifiants `sanctuary:hunter` et `sanctuary:hunter_spawn_egg`.
|
||||
Aucun bébé, y compris via œuf, chargement ou familier ; pas de noyé.
|
||||
- Sauvage : 10 PV, vitesse 0,30, mêlée 3, poursuite 36 blocs. Préparation
|
||||
de 16 ticks au sol, bond à 3–10 blocs avec ligne de vue, impulsion verticale
|
||||
0,62 et vitesse horizontale adaptée à la distance, plafonnée à 1,25.
|
||||
Une seule frappe de 4 dégâts au contact, récupération de 24 ticks,
|
||||
recharge de 120 ticks engagée au décollage, même si le bond rate.
|
||||
- Gravité, trajectoire et collisions natives ; corridor complet chargé et
|
||||
bordure vérifiés. Mur, plafond, atterrissage ou durée bornée terminent le bond.
|
||||
Une téléportation n'est pas interprétée comme un balayage d'attaque. Aucun
|
||||
bloc détruit, chunk généré ou monde existant réécrit.
|
||||
- Familier rare : adulte miniature à taille variable persistante, 12 PV,
|
||||
vitesse 0,30. Attaque de contact automatique et G de bond offensif à 8 blocs,
|
||||
avec trajectoire physique et récupération. Les protections communes des
|
||||
familiers gardent autorité sur les dégâts, destinataires, claims et duels.
|
||||
Le familier ne bondit pas lorsqu'il est monté ou transporté.
|
||||
- Œuf créatif, spawn rare dans les habitats de zombies de l'Overworld : poids 4,
|
||||
un individu. Recharge conservée au chargement ; préparation et saut interrompus
|
||||
ne reprennent pas après rechargement. Aucun identifiant ou format existant changé.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `:sanctuary:runGameTest -PsanctuaryFocusedTests=hunter286
|
||||
-PsanctuaryAtlasOnly=true` : **16 tests requis réussis**, dont le test
|
||||
d’infrastructure. Préparation accroupie, vraie élévation, contact unique,
|
||||
esquive, murs, plafond, atterrissage, recharge, sauvegarde interrompue,
|
||||
téléportation, corridor déchargé, G physique, protections communes et six
|
||||
tailles persistantes.
|
||||
- Régressions séparées : **77 tests** avec
|
||||
`ziplines242,boomer284,spitter283,charger282,infected280,familiar260,companions`,
|
||||
puis **19 tests** avec `smoker285`. Les joueurs simulés du Smoker imposent
|
||||
une session indépendante des tests d’IA asynchrones. Chaque total comprend
|
||||
son test d’infrastructure.
|
||||
- Deux arènes historiques corrigées sans changement de comportement : sol
|
||||
du Charger élargi pour garder l’esquive de quatre blocs sur la plateforme ;
|
||||
arènes du Smoker replacées dans les chunks suivis par GameTest pour que les
|
||||
recherches natives de projectiles voient immédiatement les joueurs.
|
||||
- `:sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryHunter286ClientTests=true -PsanctuaryQuickTests=true
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8
|
||||
-PsanctuaryClientNoVsync=true` : **réussi**, Vulkan / MoltenVK 1.4.2 /
|
||||
Apple M1. Monde de développement plat temporaire, graine `286` : appui G
|
||||
réel, familier en vol, contact et recharge ; bond sauvage sur le vrai joueur,
|
||||
dégâts, récupération et gravité ; esquive au clavier sans dégâts ; rendu
|
||||
adulte aux tailles 500/1000/1500 ; interface et infobulle FR/EN, GUI 2/3/4.
|
||||
Captures : `build/hunter286-screenshots/`.
|
||||
- `./gradlew check build assemblePack -PsanctuaryFocusedTests=hunter286
|
||||
-PsanctuaryAtlasOnly=true` : **réussi en 2 min 46 s**, y compris les 16
|
||||
tests Hunter rejoués et tous les contrôles purs configurés par `check`.
|
||||
- Export `packwiz modrinth export` et intégrité ZIP vérifiés : un seul JAR
|
||||
Sanctuary, identique au JAR construit ; version `beta.286`, Minecraft `26.3`,
|
||||
Fabric Loader `0.19.5`. Classes de test exclues ; classes Hunter, œuf,
|
||||
profil, butin, tags et traductions FR/EN présents. Pixels du PNG conformes
|
||||
au script, cinq textures antérieures et MRpack beta.285 inchangés.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- [Pack beta.286](../build/Sanctuary-beta.286.mrpack), 16 682 455 octets.
|
||||
SHA-256 : `9363b9067bc9974cd5f850b671e0d97c1f0876eda6885a51e2acf32d6d3b1103`.
|
||||
- JAR `mods/sanctuary/build/libs/sanctuary-beta.286.jar`.
|
||||
SHA-256 : `02a62837d2385ec61cf76c915c2eb0e0cc5b339d5bd3ceab64a2f1086bc94abf`.
|
||||
- Reçu détaillé : `build/hunter286-validation.json`. Logs et captures ignorés
|
||||
sous `build/hunter286-*`. Les premiers essais échoués des arènes historiques
|
||||
sont conservés à côté des résultats finaux ; les réparations ci-dessus ne
|
||||
changent aucun comportement du Charger ou du Smoker.
|
||||
|
||||
## Limites
|
||||
|
||||
Le Hunter saute puis retombe : aucun plaquage ou maintien du joueur. La vitesse
|
||||
de lancement adapte la portée au sol ; un dénivelé, un obstacle ou le déplacement
|
||||
de la cible peut faire rater le contact. Seuls les chunks déjà chargés sont
|
||||
utilisés. Aucune validation graphique OpenGL, publication, synchronisation
|
||||
Prism, installation personnelle ou modification de sauvegarde existante.
|
||||
@@ -0,0 +1,76 @@
|
||||
# ZOMBIES-281 — Infectés adultes et familiers miniatures
|
||||
|
||||
Branche `codex/special-zombies`, suite au retour sur beta.280. État : livré et
|
||||
vérifié localement.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les infectés n'ont **aucune forme bébé**. Leur modèle et leur texture sont
|
||||
toujours ceux de l'adulte. Cette règle appartient à `AbstractInfectedEntity`,
|
||||
le socle commun des variantes : les futurs Hunter, Charger, Spitter et Boomer
|
||||
devront aussi en hériter. Ces variantes restent à implémenter.
|
||||
|
||||
L'Infecté actuel refuse le flag bébé des groupes naturels, des appels natifs,
|
||||
des œufs et du chargement des données d'entité. Il ne reçoit pas le bonus de
|
||||
vitesse bébé. Les bébés zombies Minecraft gardent leurs règles habituelles.
|
||||
|
||||
En familier, l'adulte est réduit avec le système `FamiliarSize` existant.
|
||||
Son trait de taille enregistré dans l'œuf reste identique : petites, ordinaires,
|
||||
grandes et colossales restent possibles. Le rendu en jeu, les aperçus des menus,
|
||||
les dimensions de monture et les sons utilisent tous la même forme adulte.
|
||||
La taille ne dépend donc plus d'un âge bébé.
|
||||
|
||||
Les identifiants, la texture vanilla recolorée, les règles de combat et les
|
||||
formats restent ceux de beta.280. Une ancienne donnée d'Infecté portant
|
||||
`IsBaby=true` est interprétée comme adulte à la lecture, sans changement de
|
||||
son UUID ou de sa santé. Aucun monde personnel n'est ouvert ou réécrit.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- Refus du bébé et de son bonus de vitesse, avec bébé zombie vanilla conservé.
|
||||
- Lecture d'un ancien flag bébé sans perdre l'identité ni la santé.
|
||||
- Familier adulte à six tailles, de 120 à 6 000 ‰, sans changer le trait ni
|
||||
l'identité ; cohérence entre hauteur visible et hauteur de monture.
|
||||
- Parcours Vulkan sur un monde plat neuf, graine `281` : trois tailles réelles
|
||||
de familier, inspection de la géométrie adulte et menus FR/EN GUI 2/3/4.
|
||||
- `./gradlew check build assemblePack` et vérification du MRpack beta.281.
|
||||
|
||||
La suite de régression conserve son entrée `infected280` et son option client
|
||||
`sanctuaryInfected280ClientTests`, actualisées selon ce nouveau contrat.
|
||||
|
||||
Les **34 GameTests serveur** ciblés passent, dont neuf pour l'Infecté. Les
|
||||
contrôles de réapparition et des 88 familiers vanilla passent aussi. Le parcours
|
||||
client **Vulkan / MoltenVK 1.4.2 / Apple M1** passe en **54 secondes**.
|
||||
Il vérifie le vrai rendu du familier, sa géométrie adulte et sa hauteur pour
|
||||
500, 1 000 et 1 500 ‰, ainsi que les noms, infobulles et menus FR/EN aux échelles
|
||||
2, 3 et 4. Dix captures sont conservées sous `build/infected281-screenshots/` ;
|
||||
les petites et grandes miniatures et le menu FR GUI 2 ont été inspectés.
|
||||
|
||||
Logs : `build/infected281-native.log`, `build/infected281-client-vulkan.log`
|
||||
(marqueur `INFECTED281_CLIENT_PASS`). La construction complète finale
|
||||
**`check build assemblePack` passe en 3 min 09 s**, avec les 34 tests serveur.
|
||||
Log : `build/infected281-check-build-pack.log`.
|
||||
|
||||
Le JAR et le MRpack beta.281 sont vérifiés : version exacte, intégrité ZIP,
|
||||
un seul JAR Sanctuary identique au build, socle adulte commun présent,
|
||||
texture corrigée conservée octet pour octet et fixtures de test exclues.
|
||||
Reçu : `build/infected281-validation.json`.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=infected280,familiar260,companions \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryInfected280ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
## Livraison locale
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.281.jar`
|
||||
- `build/Sanctuary-beta.281.mrpack`
|
||||
- `build/infected281-screenshots/0002_infected281-miniature-1000.png`
|
||||
|
||||
Aucun déploiement dans Prism ou un serveur personnel. Les artefacts beta.280
|
||||
sont conservés ; cette livraison utilise le numéro suivant.
|
||||
@@ -0,0 +1,135 @@
|
||||
# ZOMBIES-280 — Premier infecté
|
||||
|
||||
Le contrat bébé de cette livraison est remplacé par
|
||||
[beta.281 : adultes en miniature et taille variable](infected-adults-beta281.md).
|
||||
|
||||
Branche `codex/special-zombies`. Premier incrément demandé : l'Infecté simple,
|
||||
avant les Hunter, Charger, Spitter et Boomer. État : livré localement et vérifié.
|
||||
|
||||
## Contrat
|
||||
|
||||
- `sanctuary:infected` est un zombie vert au modèle vanilla, t-shirt rouge et
|
||||
pantalon brun sombre. Adulte et bébé utilisent les formes natives.
|
||||
- Attributs de base : 10 PV, vitesse 0,25 contre 0,23 pour le zombie ; mêlée
|
||||
native, dégâts de base 3, portée de poursuite 36 blocs. Il brûle au soleil
|
||||
et ne se transforme pas en noyé. Aucun pouvoir spécial dans ce premier lot.
|
||||
- Apparition au sol dans l'obscurité, selon les règles des monstres, uniquement
|
||||
dans les biomes de l'Overworld qui proposent déjà le zombie : poids 15,
|
||||
groupes de 1 à 3. Aucun spawner ni remplacement des zombies existants.
|
||||
- `sanctuary:infected_spawn_egg` apparaît dans l'onglet des œufs créatifs et
|
||||
invoque uniquement l'Infecté. Il peut être équipé comme familier bébé avec
|
||||
identité, suivi, ordres, santé et combat persistants du système existant.
|
||||
- Le familier a 10 PV, vitesse 0,30, morsure de mêlée et assaut court. Son
|
||||
profil FR/EN est distinct du zombie. Aucune nouvelle aptitude de travail.
|
||||
- Les autres infectés et le zombie des glaces restent à réaliser. Aucune
|
||||
nouvelle voie d'acquisition des œufs en survie n'est ajoutée dans ce lot.
|
||||
|
||||
## Référence et compatibilité
|
||||
|
||||
Référence en lecture seule : `../26.2/itsalive`, commit
|
||||
`26f2c2a0061619df1f146f9081d6740c560a7497`, `InfectedVariant.INFECTED` et
|
||||
`InfectedEntity`. Le nouveau type est indépendant des cinq variantes sous
|
||||
`itsalive:infected` de l'ancien mod ; aucune importation de leur sauvegarde.
|
||||
Les identifiants, profils et données des familiers existants sont conservés.
|
||||
L'ajout de tables de spawn affecte les apparitions futures, sans changer le
|
||||
terrain, les graines, les chunks ou un format de sauvegarde.
|
||||
|
||||
La texture est une recoloration déterministe du zombie natif **26.3**, par
|
||||
masques UV et sélection des seuls pixels cyan du t-shirt et violets du pantalon.
|
||||
Le visage, la peau verte, les chaussures et l'alpha sont conservés pixel pour
|
||||
pixel. L'œuf reprend l'icône rouge historique. La première texture générée par
|
||||
ImageGen décalait l'atlas ; elle a été écartée après le retour du créateur.
|
||||
|
||||
## Vérifications
|
||||
|
||||
Les **33 GameTests serveur** ciblés passent, dont huit nouveaux : attributs,
|
||||
mêlée réelle et bébé ; utilisation de l'œuf en survie et consommation ;
|
||||
sauvegarde/relecture native avec UUID, âge et santé ; tables de spawn ;
|
||||
immersion sans conversion en noyé ; équipement et rééquipement du familier ;
|
||||
morsure réelle du familier ; changement d'œuf pendant un objectif en cours.
|
||||
Les suites de pouvoirs existants et de réapparition
|
||||
beta.260 passent avec le nouvel œuf installé.
|
||||
Log final : `build/infected280-check-build-pack.log`.
|
||||
Le serveur plat natif 26.3 utilise le désert ; le contrôle positif des tables
|
||||
porte sur cet habitat réellement générable, avec exclusions Champignons,
|
||||
Nether et End. Il ne mesure pas la fréquence des rencontres en partie longue.
|
||||
|
||||
Le parcours client final **Vulkan / MoltenVK 1.4.2 / Apple M1** passe en 54 s :
|
||||
apparences adulte et bébé face au zombie classique, familier réellement équipé,
|
||||
renderer partagé avec les proxies, texture embarquée, rareté et infobulle
|
||||
native, noms et menus **FR/EN GUI 2/3/4**. Sept captures sont conservées dans
|
||||
`build/infected280-screenshots/`. Les vues comparatives, FR GUI 4 et EN GUI 2
|
||||
ont été inspectées après correction de la texture. La vue comparative finale
|
||||
confirme le visage et les bras vanilla, le t-shirt rouge et le pantalon brun
|
||||
sans décalage. Log : `build/infected280-client-vulkan.log`, marqueur
|
||||
`INFECTED280_CLIENT_PASS`. Aucun essai OpenGL.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=infected280,familiar260,companions \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryInfected280ClientTests=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le mode plat explicite évite la fixture historique de génération, sans modifier
|
||||
le générateur de production. Tous les mondes utilisés sont jetables et ignorés.
|
||||
La construction complète finale **`check build assemblePack` passe en 2 min 59 s**,
|
||||
avec la texture corrigée et les 33 tests serveur. Les contrôles purs du dépôt
|
||||
passent également. Le MRpack est exporté et vérifié : intégrité ZIP,
|
||||
Minecraft 26.3, Fabric Loader 0.19.5, un seul JAR Sanctuary beta.280 identique
|
||||
au build et texture corrigée présente octet pour octet. Les fixtures de test
|
||||
sont exclues du JAR. Reçu : `build/infected280-validation.json`.
|
||||
|
||||
Le second parcours a également reproduit un plantage lors du remplacement
|
||||
d'un œuf : l'ancien objectif d'IA pouvait continuer un tick avant la création
|
||||
du nouvel individu. Le cerveau vérifie désormais que le familier est actif
|
||||
et que son œuf correspond encore à celui équipé avant d'agir. Un huitième
|
||||
GameTest couvre cet intervalle ; le parcours client final inclut le correctif.
|
||||
|
||||
## Essai local
|
||||
|
||||
Dans un monde de développement avec commandes autorisées :
|
||||
|
||||
```mcfunction
|
||||
/give @s sanctuary:infected_spawn_egg
|
||||
/summon sanctuary:infected
|
||||
```
|
||||
|
||||
L'œuf placé dans l'emplacement Familier invoque sa forme bébé. Les commandes
|
||||
Alt bref / Alt maintenu et la technique G restent celles des autres familiers.
|
||||
L'œuf est fourni en créatif ; aucune nouvelle acquisition en survie n'est
|
||||
annoncée. Aucun canal packwiz, serveur personnel ou instance Prism modifié.
|
||||
|
||||
## Ressources graphiques
|
||||
|
||||
- Texture livrée : `mods/sanctuary/src/main/resources/assets/sanctuary/textures/entity/infected.png`.
|
||||
- Œuf historique : `mods/sanctuary/src/main/resources/assets/sanctuary/textures/item/infected_spawn_egg.png`.
|
||||
- Outil de texture : `scripts/generate_infected_texture.py`, Python standard et
|
||||
ImageMagick 7 pour lire/écrire le PNG. **Aucun dessin génératif ni changement
|
||||
de géométrie.** 331 pixels de t-shirt et 239 pixels de pantalon recolorés,
|
||||
nuances natives conservées ; visage et transparence vérifiés avant encodage,
|
||||
puis tous les pixels vérifiés après écriture.
|
||||
- SHA-256 du PNG source :
|
||||
`1b88eea9b63889c2c3399773f887af375defc3aad0e7a375adc84249da54cbbd`.
|
||||
- SHA-256 de la texture livrée :
|
||||
`398898aed83ec22ef525b60da120fc3e0b971b33ad3640c73104b952ad060e29`.
|
||||
|
||||
Reproduction sans modifier le JAR source :
|
||||
|
||||
```sh
|
||||
python3 scripts/generate_infected_texture.py /chemin/vers/minecraft-26.3-client.jar
|
||||
```
|
||||
|
||||
Voir aussi la provenance dans `THIRD_PARTY_NOTICES.md`.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- Mod : `mods/sanctuary/build/libs/sanctuary-beta.280.jar`.
|
||||
- Pack : `build/Sanctuary-beta.280.mrpack`.
|
||||
- Captures corrigées : `build/infected280-screenshots/`.
|
||||
|
||||
La livraison est locale. La publication et la mise à jour de l'instance Prism
|
||||
restent distinctes de ce travail de développement.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Prochaine passe — mods du pack et modules personnels
|
||||
|
||||
Cadrage du **26 octobre 2026**, à la demande de Koka. Initialement préparés
|
||||
pour après beta.291, les quatre tickets d'intégration sont désormais repris
|
||||
dans [la préparation locale beta.292](optional-mods-beta292.md), à sa demande.
|
||||
Dozei Stream et Only Fun restent des contrats futurs. Chaque implémentation
|
||||
prendra sa propre branche `codex/<sujet>` et son prochain compteur libre.
|
||||
Les versions du cadrage ci-dessous sont des candidats repérés pour **Minecraft 26.3,
|
||||
Fabric 0.19.5 et Java 25**, pas des intégrations validées. Tous les essais
|
||||
graphiques cibleront Vulkan, avec Sodium puis sans si le mod le permet.
|
||||
|
||||
L'API Modrinth a été interrogée avec les filtres exacts `game_versions=["26.3"]`
|
||||
et `loaders=["fabric"]`. Les JAR candidats ont été téléchargés dans le dossier
|
||||
ignoré `build/optional-mods291/`, vérifiés contre leur SHA-512 publié et leurs
|
||||
`fabric.mod.json` relus. Revalider versions, dépendances, licences et hashes au
|
||||
début de chaque ticket ; aucun binaire téléchargé n'entre dans Git.
|
||||
|
||||
| Ticket | Branche prévue | Candidat observé | Résultat à obtenir |
|
||||
| --- | --- | --- | --- |
|
||||
| PACK-VOICE-01 | `codex/simple-voice-chat` | [2.6.26+26.3, Fabric, bêta](https://modrinth.com/plugin/simple-voice-chat/version/cZm965sW) | Conversation de proximité client/serveur, compatible avec l'acoustique Sanctuary |
|
||||
| PACK-BOBBY-01 | `codex/bobby-optional` | [5.2.16+mc26.3](https://modrinth.com/mod/bobby/version/J7YnfCss) | Voir les chunks déjà reçus au-delà de la distance envoyée par le serveur, installation facultative |
|
||||
| PACK-LITHIUM-01 | `codex/lithium-integration` | [0.26.2+mc26.3, Fabric](https://modrinth.com/mod/lithium/version/xS0Q8LSi) | Réduire le coût de simulation en conservant les règles Sanctuary |
|
||||
| PACK-CULL-01 | `codex/entity-culling` | [1.11.3 pour 26.3, Fabric](https://modrinth.com/mod/entityculling/version/X1g3VhTx) | Éviter le rendu des entités et blocs entités masqués, sans disparitions incorrectes |
|
||||
| MODULE-DOZEI-01 | `codex/dozei-stream-contract` | Pas encore réalisé, aucun artefact fourni | Définir le premier résultat vérifiable de Dozei Stream |
|
||||
| MODULE-ONLYFUN-01 | `codex/only-fun-contract` | Mod personnel futur | Définir une première interaction potache autonome |
|
||||
|
||||
## PACK-VOICE-01 — Simple Voice Chat
|
||||
|
||||
Le JAR 2.6.26 déclare `minecraft=26.3.x`, Java ≥25, Loader ≥0.19.3 ;
|
||||
Fabric API doit être ≥0.160.5+26.3, ce qui correspond au socle actuel.
|
||||
**Conflit constaté : il déclare `breaks.sound_physics_remastered=<1.5.2`,
|
||||
alors que Sanctuary distribue 1.5.1+26.3.** Le candidat 2.6.25 a le même refus.
|
||||
Le JAR [2.6.24+26.3](https://modrinth.com/plugin/simple-voice-chat/version/OLnMVWXy)
|
||||
ne déclare pas ce conflit ; cela ne prouve pas sa compatibilité en partie.
|
||||
|
||||
Décider et tester soit une mise à jour délibérée de Sound Physics et de son
|
||||
adaptateur natif Sanctuary, soit une version vocale antérieure compatible.
|
||||
Ne pas contourner les dépendances de Fabric. Conserver un artefact autonome
|
||||
provenant de l'auteur et ses conditions de distribution.
|
||||
|
||||
Acceptation : connexion de deux clients de développement, voix de proximité,
|
||||
muet, push-to-talk, déconnexion/reconnexion, coexistence Sound Physics et
|
||||
préférences conservées. Vérifier les profils packwiz client/serveur, le refus
|
||||
des versions incompatibles et les interfaces FR/EN ajoutées par Sanctuary.
|
||||
L'activation réseau de la Beta sera une étape de livraison distincte : le
|
||||
[protocole amont](https://dev.modrepo.de/minecraft/voicechat/wiki/server_setup)
|
||||
utilise UDP et un port vocal propre, par défaut 24454. Tester depuis l'extérieur
|
||||
avec `/voicechat test`, puis un échange audible ; l'ouverture du port Minecraft
|
||||
TCP ne suffit pas. Aucun port ni serveur personnel n'est modifié par ce ticket
|
||||
documentaire.
|
||||
|
||||
## PACK-BOBBY-01 — cache client facultatif
|
||||
|
||||
Le candidat déclare un environnement client, Minecraft `~26.3`, Loader
|
||||
≥0.18.4 et refuse Sodium <0.6.0 ; Sodium 0.9.2 du pack satisfait ce seul
|
||||
contrat déclaré. Cloth Config et Mod Menu sont suggérés, pas requis dans le
|
||||
JAR inspecté. Le [principe amont](https://github.com/Johni0702/bobby) est de
|
||||
mettre en cache les chunks envoyés dans `.bobby`, puis d'afficher leur dernière
|
||||
copie connue à distance. Ce cache ne simule pas les chunks et ne remplace pas
|
||||
leur état serveur actuel.
|
||||
|
||||
Acceptation : choix packwiz **facultatif**, installation/désinstallation par
|
||||
le sélecteur et préférence conservée au second lancement ; parcours puis
|
||||
retour sur un serveur de développement avec distance d'envoi courte ; chunks
|
||||
lointains visibles sous Vulkan, actualisation lors de leur réception réelle,
|
||||
claims/Atlas et blocs Sanctuary cohérents au retour. Aucun monde personnel
|
||||
utilisé comme `bobby-fallback`. Définir une valeur initiale de distance et une
|
||||
politique de cache mesurées avant livraison ; pas de génération supplémentaire
|
||||
imposée au serveur.
|
||||
|
||||
## PACK-LITHIUM-01 — simulation
|
||||
|
||||
Le [candidat](https://github.com/CaffeineMC/lithium) peut fonctionner côté client
|
||||
et serveur. Ses dépendances déclarées sont Minecraft `~26.3`, Loader ≥0.19.5
|
||||
et MixinExtras ≥0.5.5. Vérifier la version réellement fournie par Loader ; ne
|
||||
pas ajouter une bibliothèque par supposition. Définir séparément les profils
|
||||
serveur et client, sans faire dépendre l'autorité serveur du choix d'un joueur.
|
||||
|
||||
Acceptation : démarrage et rechargement d'un monde de test, recettes et
|
||||
inventaires, IA des infectés/familiers/baleines, interactions multiblocs et
|
||||
budget de ticks comparés avant/après sur le même scénario. Les gains doivent
|
||||
être mesurés ; conserver les identifiants, sauvegardes et règles serveur.
|
||||
|
||||
## PACK-CULL-01 — masquage visuel
|
||||
|
||||
Le [candidat](https://github.com/tr7zw/EntityCulling) cible Minecraft `~26.3`
|
||||
et requiert Fabric API ; TRansition et TRender sont déjà imbriqués dans le
|
||||
JAR inspecté. Vérifier leurs hooks de rendu sur Vulkan avant toute inclusion,
|
||||
ainsi que les conditions de distribution du binaire original.
|
||||
|
||||
Acceptation : comparaison activé/désactivé sur une scène de développement,
|
||||
avec mur opaque, fenêtres, verre, sortie d'occlusion et changement rapide de
|
||||
caméra. Vérifier familiers portés, cosmétiques, baleines translucides, coffres
|
||||
et multiblocs ; aucune disparition visible incorrecte. Mesurer le gain et le
|
||||
coût CPU. Aucun essai OpenGL ne vaut validation de ce ticket.
|
||||
|
||||
## Modules personnels — contrats à écrire
|
||||
|
||||
**MODULE-DOZEI-01 :** conserver le nom donné « Dozei Stream » ; son rôle,
|
||||
ses sources, sa cible client/serveur, ses dépendances et son premier scénario
|
||||
restent à préciser avec le créateur. Ne déduire aucune fonctionnalité du nom.
|
||||
Acceptation du cadrage : une action observable et un périmètre testable avant
|
||||
de créer un faux artefact ou une entrée packwiz sans téléchargement.
|
||||
|
||||
**MODULE-ONLYFUN-01 :** reprendre la [vision Only Fun](vision.md#only-fun--interactions-sociales-et-absurdes),
|
||||
avec l'intention confirmée d'ajouter de l'humour « pipi caca ». C'est un mod
|
||||
personnel autonome, séparé du socle nécessaire pour jouer à Sanctuary.
|
||||
Choisir une première interaction, définir ses entrées, effets, données
|
||||
persistantes et branchements facultatifs au calendrier/aux événements.
|
||||
Acceptation du cadrage : petit scénario jouable défini, comportement en solo
|
||||
et serveur, installation indépendante et libellés FR/EN prévus. Le contenu
|
||||
futur de la vision ne devient pas une fonctionnalité livrée par ce document.
|
||||
|
||||
## Ordre proposé et fin d'un ticket
|
||||
|
||||
Commencer par Bobby facultatif, puis les optimisations mesurées ; traiter la
|
||||
voix avec sa décision Sound Physics et son parcours réseau. Les deux modules
|
||||
personnels commencent par leur contrat, sans dépendance implicite à ces mods.
|
||||
Cet ordre reste une proposition pour la prochaine séance.
|
||||
|
||||
Chaque intégration livrée passe `./gradlew check build assemblePack`, les
|
||||
essais propres au comportement ajouté, deux synchronisations packwiz isolées,
|
||||
la conservation des choix et le contrôle du profil serveur. Décrire les
|
||||
résultats réels et les limites restantes avant toute publication demandée.
|
||||
@@ -0,0 +1,126 @@
|
||||
# MAP-279 — Carte des îles et grand dézoom
|
||||
|
||||
Branche `codex/map-islands-zoom`, base beta.278 ; livraison locale beta.279,
|
||||
Minecraft 26.3, demande du 25 octobre 2026.
|
||||
|
||||
## Résultat
|
||||
|
||||
La limite de zoom liée à une fenêtre de 1 024 blocs disparaît. La molette et
|
||||
les boutons permettent de passer du détail des blocs à une vue de plusieurs
|
||||
îles, puis à une emprise de 67 108 864 blocs, suffisante pour cadrer le monde.
|
||||
Le dézoom reste centré sous le pointeur ; les boutons gardent le centre de la
|
||||
vue. Les zones découvertes éloignées sont affichées ensemble ; les zones non
|
||||
relevées restent inconnues. Le centre de la carte peut rester fixe pendant que
|
||||
le joueur se déplace et reçoit de nouveaux chunks.
|
||||
|
||||
Le message « Carte pleine : nouveaux relevés suspendus » provenait du plafond
|
||||
de 16 384 chunks par couche et habitant. Il passe à **131 072**, sans éviction
|
||||
des découvertes. Une carte ayant atteint l'ancien plafond reprend ses relevés
|
||||
lorsque les chunks sont de nouveau reçus ou revisités. Les parcours non
|
||||
conservés avant ce correctif ne peuvent pas être reconstitués automatiquement.
|
||||
Cette capacité reste bornée ; ce lot ne promet pas une exploration illimitée.
|
||||
|
||||
## Rendu et serveur
|
||||
|
||||
La texture reste de 1 024 × 1 024 pixels. Seize niveaux de réduction regroupent
|
||||
les couleurs par carrés de 2, 4, 8… jusqu'à 65 536 blocs de côté par texel.
|
||||
Le niveau demandé dépend de la fenêtre et du zoom ; le serveur ne transmet
|
||||
que les tuiles de ce niveau dans le viewport, par lots de 64 maximum.
|
||||
Les caches réduits sont calculés à partir des seuls relevés personnels,
|
||||
à la demande, et invalidés par les changements de terrain. Les couleurs de
|
||||
terrain sont privilégiées sur le vide relevé pour garder les petits îlots.
|
||||
Ce choix accentue les très petites zones et n'est pas une mesure de leur aire.
|
||||
|
||||
Les six couches d'altitude restent distinctes. Les positions des joueurs et
|
||||
les marqueurs utilisent la même emprise étendue. La vue opérateur réduit
|
||||
uniquement les chunks déjà chargés, échantillonnés à son budget existant de
|
||||
quatre chunks par tick ; elle reste séparée des relevés personnels.
|
||||
Les permissions, jetons, dimensions et cadences de requête restent contrôlés.
|
||||
L'aperçu Pause conserve son échelle proche et bénéficie de la capacité accrue.
|
||||
|
||||
Les contours des propriétés, Demeure et le clic droit sur un chunk restent
|
||||
accessibles au niveau détaillé ; la vue d'ensemble indique en FR/EN qu'il faut
|
||||
zoomer pour les retrouver. La grille s'efface lorsque ses lignes seraient
|
||||
espacées de moins de huit pixels. L'échelle affiche les blocs par pixel au
|
||||
lieu d'un multiplicateur arrondi à zéro.
|
||||
|
||||
## Persistance et compatibilité
|
||||
|
||||
Les fichiers `data/sanctuary-atlas/<UUID>.json` et leurs six couches gardent
|
||||
le schéma 2, les mêmes coordonnées, couleurs, propriétaires et graines.
|
||||
Les lecteurs historiques de schéma 1 restent identiques. Aucun niveau réduit
|
||||
n'est écrit dans les sauvegardes. La limite de taille acceptée passe de 12 à
|
||||
64 Mio par fichier pour accueillir le plafond élargi ; les écritures atomiques
|
||||
et le refus d'écraser un fichier modifié extérieurement sont conservés.
|
||||
Aucun changement de génération, chunk, identifiant ou expansion.
|
||||
|
||||
Le protocole `sanctuary:atlas_request` ajoute le niveau de détail : mettre à
|
||||
jour client et serveur ensemble. Un retour vers beta.278 exige la copie
|
||||
antérieure des relevés dès qu'une couche a dépassé 16 384 chunks : l'ancien
|
||||
lecteur refuse un tel fichier. Aucune sauvegarde personnelle ni instance de
|
||||
jeu n'est ouverte ou modifiée par ce ticket.
|
||||
|
||||
## Vérification
|
||||
|
||||
Les contrôles purs couvrent les seize réductions, les coordonnées négatives,
|
||||
le bord du monde, la conservation d'un petit îlot, l'invalidation après une
|
||||
construction, les zones inconnues, les bornes des requêtes et les marqueurs
|
||||
lointains. Le schéma 2 est sauvegardé puis relu avec 17 000 chunks ; le plafond
|
||||
courant est aussi atteint sans éviction.
|
||||
|
||||
Commandes de validation :
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=map244,map,claims273,claims277,waystones239 \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryMap279ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan \
|
||||
-PsanctuaryClientHeapGb=8 -PsanctuaryClientNoVsync=true
|
||||
```
|
||||
|
||||
Le parcours graphique utilise un monde plat de développement neuf, graine
|
||||
`279`, et cinq relevés de contrôle espacés de 8 192 blocs pour isoler les
|
||||
problèmes d'échelle. Il ne constitue pas une visite des cinq continents
|
||||
Sanctuary ni un benchmark multijoueur. Les déplacements lointains, envois
|
||||
natifs de chunks et changements de blocs utilisent le vrai serveur intégré.
|
||||
Le parcours **Vulkan / MoltenVK 1.4.2 / Apple M1** passe en **1 min 07 s**,
|
||||
avec **11 captures**. Il vérifie les cinq relevés simultanés, une arrivée réelle
|
||||
à 9 008 blocs sans déplacer le centre de la carte ni changer son jeton,
|
||||
un bloc d'or natif nouvellement relevé, un dézoom au-delà de 256 blocs par pixel puis au
|
||||
niveau maximal, l'emprise du monde sur les deux axes, le retour à la couleur
|
||||
exacte du bloc, Pause, FR/EN GUI 2/3/4 et le retrait de l'aptitude Atlas.
|
||||
Une tuile chargée pour le seul contrôle opérateur, à 12 000 blocs, apparaît
|
||||
dans son aperçu réduit sans entrer dans les relevés personnels, puis disparaît
|
||||
quand les commandes sont retirées. Le chargement forcé appartient uniquement
|
||||
à la fixture du monde de test ; les requêtes Atlas n'en créent pas.
|
||||
|
||||
Les captures de vue d'ensemble FR GUI 4, de retour au détail, d'échelle du
|
||||
monde et d'opérateur ont été inspectées. Les cinq petits carrés colorés sont
|
||||
des relevés de contrôle, pas des captures du relief des continents.
|
||||
Captures : `build/map279-screenshots/` ; log :
|
||||
`build/map279-client-vulkan.log`, marqueur `MAP279_CLIENT_PASS`.
|
||||
|
||||
La construction complète et les **20 tests serveur ciblés** passent : relevé
|
||||
natif au-delà de l'ancien plafond, six couches, absence de découverte chez un
|
||||
autre habitant ou un spectateur, droits Atlas, claims, Demeure et Waystones.
|
||||
Le log est `build/map279-check-build-pack.log`. Les contrôles purs du dépôt
|
||||
passent également. La capacité maximale en multijoueur et le coût de sauvegarde
|
||||
d'une couche entièrement remplie ne sont pas mesurés par ce lot.
|
||||
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
La dernière passe `check build assemblePack` passe en **3 min 54 s**.
|
||||
Le MRpack est exporté et vérifié : intégrité ZIP, Minecraft 26.3, Loader 0.19.5,
|
||||
un seul JAR Sanctuary beta.279 identique au build local, nouvelle pyramide
|
||||
présente, fixtures de tests absentes et trois libellés FR/EN embarqués. Le JAR
|
||||
JEI est également identique au build local ; les trois dépendances distantes
|
||||
gardent leurs empreintes. Reçu : `build/map279-validation.json`.
|
||||
|
||||
- JAR : `mods/sanctuary/build/libs/sanctuary-beta.279.jar`, 16 239 330 octets.
|
||||
- MRpack : `build/Sanctuary-beta.279.mrpack`, 16 593 044 octets.
|
||||
- SHA-256 du MRpack : `1ebe43f559cec3727fea17ed8cf5593702109ceb6151f5e45125beda83cda6c8`.
|
||||
|
||||
Cette livraison est locale : aucun canal, tag distant, serveur personnel ou
|
||||
instance Prism n'est modifié.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Sanctuary beta.289
|
||||
|
||||
Six nouveaux infectés rejoignent Sanctuary : Infecté, Charger, Spitter, Boomer,
|
||||
Smoker et Hunter, chacun avec son œuf et son comportement. Le Spitter laisse
|
||||
une flaque d’acide ; le Smoker doit toucher avec son lasso avant de tirer sa
|
||||
cible ; le Hunter prépare un bond qui peut être esquivé.
|
||||
|
||||
Neuf tenues aléatoires sont partagées avec le zombie classique. Les bras restent
|
||||
en avant, avec le Hunter accroupi au sol. Les infectés restent sans variante
|
||||
bébé ; leurs familiers reprennent le modèle adulte en miniature, à taille
|
||||
variable, avec leurs pouvoirs.
|
||||
|
||||
Les objets lumineux tenus en main éclairent de nouveau le terrain avec Sodium,
|
||||
y compris sans PBR. JEI est intégré au même JAR Sanctuary sur le client et le
|
||||
serveur. Le grand dézoom de carte et les relevés lointains sont conservés.
|
||||
|
||||
Minecraft 26.3 · Fabric 0.19.5. Construction complète, 31 tests serveur et parcours
|
||||
Vulkan des zombies/familiers vérifiés pour cette livraison. Les lumières utilisent
|
||||
le correctif de beta.288, dont les classes sont conservées à l’identique.
|
||||
|
||||
Le téléchargement du site reste l’instance Prism prête à suivre le canal stable.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Sanctuary beta.293
|
||||
|
||||
- Happy Creepers repris de la branche dédiée `codex/happy-creepers` : Maj +
|
||||
clic droit avec une fleur pour la porter, visage souriant et comportement
|
||||
passif ; retrait à main vide avec le même geste. Charge électrique conservée.
|
||||
- Baleines fantômes volantes et œuf natif de beta.290 inclus.
|
||||
- Logo Sanctuary affiché pendant les téléchargements et mises à jour packwiz,
|
||||
avec l'installateur intégré au ZIP Prism prêt à importer.
|
||||
- Simple Voice Chat 2.6.24+26.3, Bobby 5.2.16+mc26.3, Lithium 0.26.2+mc26.3 et
|
||||
Entity Culling 1.11.3 pour 26.3 disponibles comme choix facultatifs du client.
|
||||
Lithium et la voix font également partie du profil serveur.
|
||||
- Sound Physics 1.5.1, Sodium 0.9.2, JEI embarqué et Minecraft 26.3 / Fabric
|
||||
0.19.5 conservés. La version vocale retenue respecte ces dépendances.
|
||||
- Sauvegardes et réglages restent attachés à la même instance ; aucun changement
|
||||
de génération ou de format de monde. Dozei Stream et Only Fun restent futurs.
|
||||
|
||||
Le ZIP Prism suit le canal stable à chaque lancement. Les anciens utilisateurs
|
||||
gardent leur instance ; la première mise à jour de l'habillage doit aussi
|
||||
installer le nouvel outil packwiz et conserver `--bootstrap-no-update`.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Sanctuary beta.295
|
||||
|
||||
Le serveur **Sanctuary Beta** est enregistré automatiquement au premier
|
||||
lancement du client, avec le logo du modpack. Il utilise l'adresse publique
|
||||
`beta.sanctuary-minecraft.net`. Les autres favoris restent conservés ; vous
|
||||
pouvez ensuite renommer, déplacer ou supprimer cette entrée.
|
||||
|
||||
Le serveur annonce aussi le logo Sanctuary dans la liste multijoueur.
|
||||
Le même parcours s'applique au ZIP Prism du site, au MRpack et aux instances
|
||||
existantes qui suivent le canal packwiz.
|
||||
|
||||
La commande de propriété est unifiée : **`/sanctuary property`**, avec
|
||||
`claim`, `protect` et `duel`. Les alias `propriete` et `propriété` sont retirés.
|
||||
Les règles, prix et protections des claims restent identiques.
|
||||
|
||||
Les sauvegardes et paramètres de génération sont conservés ; aucun nouveau
|
||||
monde n'est nécessaire.
|
||||
@@ -0,0 +1,164 @@
|
||||
# PACK-292 — voix, cache de chunks et optimisations
|
||||
|
||||
Préparation locale du **26 octobre 2026**, branche
|
||||
`codex/optional-mods-integration`, depuis le logo packwiz beta.291.
|
||||
Le même logo et le même canal stable sont conservés.
|
||||
|
||||
## Mods intégrés
|
||||
|
||||
| Mod | Binaire retenu pour Minecraft 26.3 / Fabric | Client | Serveur |
|
||||
| --- | --- | --- | --- |
|
||||
| [Simple Voice Chat](https://modrinth.com/plugin/simple-voice-chat/version/OLnMVWXy) | 2.6.24+26.3, bêta | Facultatif, coché initialement | Inclus, activé initialement |
|
||||
| [Bobby](https://modrinth.com/mod/bobby/version/J7YnfCss) | 5.2.16+mc26.3 | Facultatif, coché initialement | Exclu |
|
||||
| [Lithium](https://modrinth.com/mod/lithium/version/xS0Q8LSi) | 0.26.2+mc26.3 | Facultatif, coché initialement | Inclus, activé initialement |
|
||||
| [Entity Culling](https://modrinth.com/mod/entityculling/version/X1g3VhTx) | 1.11.3 pour mc26.3 | Facultatif, coché initialement | Exclu |
|
||||
|
||||
Chaque téléchargement utilise le binaire original, sa version Modrinth
|
||||
immuable et son SHA-512 dans `packwiz/mods/*.pw.toml`. Les JAR restent séparés,
|
||||
sans dépendance obligatoire ni copie imbriquée dans le JAR Sanctuary. Les
|
||||
descriptions du sélecteur packwiz sont en FR/EN. Les interfaces amont conservent
|
||||
leurs propres traductions, vérifiées pour le menu vocal français et anglais.
|
||||
|
||||
`scripts/data/optional-mods.json` fixe les identifiants, versions, côtés,
|
||||
dépendances et conflits attendus. `verify_optional_mods.py`, exécuté par
|
||||
`check` et `assemblePack`, vérifie les métadonnées Fabric des JAR, leur hash et
|
||||
les bibliothèques imbriquées. Les quatre fichiers réellement utilisés par
|
||||
Gradle sont aussi comparés aux binaires vérifiés pendant la validation.
|
||||
Minecraft reste **26.3**, Loader **0.19.5**, Java **25**, Fabric API
|
||||
**0.160.5+26.3** ; MixinExtras **0.5.5** est fourni par Loader et satisfait Lithium.
|
||||
|
||||
## Choix de version vocale
|
||||
|
||||
Les versions vocales 2.6.25 et 2.6.26 refusent explicitement Sound Physics
|
||||
Remastered <1.5.2. Le pack conserve son **1.5.1+26.3** et son interface native,
|
||||
donc la voix est épinglée en **2.6.24+26.3**, sans contournement des dépendances.
|
||||
Le plug-in Sound Physics est chargé par le serveur vocal et la connexion locale
|
||||
est authentifiée pendant le parcours client. Les mises à jour de ces deux mods
|
||||
doivent être traitées ensemble à la prochaine évolution de version.
|
||||
|
||||
Le menu vocal s'ouvre avec sa touche amont **V**, configurable ; la touche de
|
||||
push-to-talk se choisit dans les réglages vocaux. Aucun raccourci ni préférence
|
||||
personnelle n'est remplacé par le pack. La voix reste facultative pour le joueur.
|
||||
|
||||
Le profil serveur contient désormais **Fabric API, Sanctuary, Lithium et Simple
|
||||
Voice Chat**. Son choix est indépendant des options d'un client. Le kit Linux
|
||||
reste sans interface ; son installateur officiel est conservé. La vérification
|
||||
Web compare aussi les nouveaux JAR serveur au canal avant toute publication.
|
||||
|
||||
L'utilisation sur la Beta dédiée exigera le
|
||||
[port UDP vocal](https://dev.modrepo.de/minecraft/voicechat/wiki/server_setup),
|
||||
par défaut 24454, et un échange audible entre deux joueurs. Cette préparation
|
||||
ne modifie ni la CT Beta, ni ses règles réseau, ni l'instance personnelle. Le
|
||||
microphone est désactivé dans le test de développement ; aucune captation de la
|
||||
voix réelle de l'utilisateur n'est nécessaire à la validation réseau locale.
|
||||
|
||||
## Portée de Bobby et d'Entity Culling
|
||||
|
||||
[Bobby](https://github.com/Johni0702/bobby) conserve les chunks déjà envoyés
|
||||
par le serveur dans son cache client `.bobby`, puis peut afficher leur dernière
|
||||
copie connue au-delà de la distance d'envoi. Il ne génère pas de chunks et ne
|
||||
simule pas les zones lointaines. Le serveur reste autoritaire lorsqu'il renvoie
|
||||
l'état courant. Le pack ne distribue aucun monde `bobby-fallback`, cache, ni
|
||||
configuration personnelle ; la limite de distance et la conservation du cache
|
||||
restent celles du mod et des réglages du joueur.
|
||||
|
||||
[Entity Culling](https://github.com/tr7zw/EntityCulling) conserve son moteur
|
||||
amont et ses bibliothèques TRender/TRansition imbriquées. L'occlusion et la
|
||||
réapparition d'une entité sont exercées sur Vulkan. Aucun gain chiffré de FPS
|
||||
ou de ticks n'est revendiqué ; les validations vérifient ici le fonctionnement
|
||||
et la coexistence, sans benchmark représentatif de tous les mondes Sanctuary.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- `./gradlew check build assemblePack assembleServerPack
|
||||
-PsanctuaryFocusedTests=recipes,progression,smoker285,hunter286
|
||||
-PsanctuaryAtlasOnly=true -PsanctuaryOptional292ServerTests=true` :
|
||||
**réussi en 2 min 52 s**, **46 tests serveur requis réussis**, Lithium et voix
|
||||
effectivement chargés. Recettes, progression, lasso du Smoker et bond du Hunter
|
||||
sont exercés avec ces dépendances. Les contrôles de ressources, de packwiz et
|
||||
de l'habillage passent aussi. Le test de quota de baleines déjà en échec dans
|
||||
[beta.291](packwiz-logo-beta291.md) reste hors de ce périmètre, sans correction
|
||||
ni validation nouvelle de ce cas.
|
||||
- Parcours graphique **Vulkan / MoltenVK 1.4.2 / Apple M1 avec Sodium 0.9.2**,
|
||||
réussi en **38 s** : quatre mods présents, Sound Physics initialisé, entité
|
||||
masquée derrière le mur puis visible après retrait ; Bobby actif, stockage
|
||||
persistant et **35 chunks de cache** visibles après un déplacement de huit
|
||||
chunks avec une distance d'envoi de quatre. Authentification et connexion UDP
|
||||
vocales réelles, menus vocaux FR/EN ouverts en gardant le microphone muet.
|
||||
Les captures du mur, de l'entité réapparue et du menu FR sont inspectées.
|
||||
- Le même parcours **Vulkan sans Sodium** réussit en **43 s**, avec les mêmes
|
||||
contrôles, 35 chunks de cache et menus FR/EN. Un premier passage avait atteint
|
||||
tous les contrôles avant de se bloquer dans la synchronisation des threads du
|
||||
lanceur Fabric pendant la fermeture du monde ; il a été interrompu après
|
||||
capture des piles. La nouvelle exécution se ferme normalement. Cette limite
|
||||
du premier essai reste dans le journal, sans la présenter comme un succès.
|
||||
- Deux synchronisations client depuis le ZIP Prism réussies : **huit JAR**,
|
||||
un seul Sanctuary, mêmes hashes au second passage, logo de l'installateur
|
||||
conservé et trois fichiers témoins personnels inchangés. Puis deux passages
|
||||
supplémentaires avec un choix Bobby désactivé déjà persisté, simulé dans le
|
||||
cache de test : Bobby n'est pas réinstallé et le choix reste faux.
|
||||
- Deux synchronisations serveur réussies : exactement les quatre JAR du
|
||||
profil serveur ; aucun Bobby, Entity Culling, Sodium ou Sound Physics client.
|
||||
- Les **11 tests** du vérificateur Web passent, y compris la comparaison des
|
||||
dépendances partagées avec le relevé serveur et le refus d'un mod client dans
|
||||
son profil. Le MRpack exporté possède les sept dépendances distantes et leurs
|
||||
bons côtés et options ; les overrides sont limités à Sanctuary et son icône.
|
||||
- Contrôle complémentaire des baleines avec les quatre mods, Sodium et Vulkan :
|
||||
les assertions de rendu translucide, modèle animé, œuf FR/EN, deux entités
|
||||
chargées et déplacement réel atteignent `WHALE290_CLIENT_PASS`. La capture
|
||||
`0001_whale290-ghosts.png` est inspectée. Ce parcours se bloque ensuite à la
|
||||
fermeture dans la synchronisation du lanceur de tests Fabric et est arrêté
|
||||
après capture des piles ; il ne constitue pas une exécution complète réussie.
|
||||
- Après ajout des notices, le JAR et les archives sont réassemblés et les
|
||||
synchronisations sont répétées sur les binaires finaux. Les **2 658 classes
|
||||
de jeu** restent identiques à beta.291. Le vérificateur Web accepte le MRpack
|
||||
et le ZIP Prism finaux contre les métadonnées préparées et les hashes du
|
||||
serveur de test synchronisé, avec état de service simulé pour cette vérification
|
||||
hors ligne ; aucune CT de production n'est contactée.
|
||||
|
||||
Les essais utilisent uniquement des mondes et dossiers de développement sous
|
||||
`build/`. Les copies et hashes finaux sont consignés dans
|
||||
`build/optional-mods292/`, avec les journaux, captures et reçus natifs.
|
||||
Le passage vocal audible à deux joueurs, l'accès UDP depuis Internet et les
|
||||
rendus propres aux familiers et multiblocs avec culling restent des
|
||||
vérifications complémentaires à effectuer avant d'en revendiquer la couverture.
|
||||
|
||||
Commande du parcours client principal (ajouter `-PsanctuarySodiumClientTests=true`
|
||||
pour le passage avec Sodium) :
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryOptional292ClientTests=true \
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8 \
|
||||
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true \
|
||||
-PsanctuaryFocusedTests=recipes,progression
|
||||
```
|
||||
|
||||
L'option supplémentaire `-PsanctuaryOptional292WhaleOnly=true` sélectionne
|
||||
le diagnostic des baleines avec le même ensemble de mods ; son blocage de
|
||||
fermeture observé ci-dessus reste ouvert.
|
||||
|
||||
## Livraison et suite
|
||||
|
||||
Le compteur du pack et du mod est aligné sur **beta.292**. Les archives locales
|
||||
conservent l'habillage packwiz, les options et le canal de la Beta. Un ZIP Prism
|
||||
préparé ne fait pas avancer le canal public : avant publication, ce canal sert
|
||||
encore la version publiée. Aucune installation de jeu, sauvegarde personnelle,
|
||||
publication Web, release Gitea ou configuration réseau n'est modifiée ici.
|
||||
|
||||
Dozei Stream et Only Fun restent des modules personnels futurs, avec leurs
|
||||
[tickets de cadrage](integrations-mods-next.md). Aucun artefact fictif n'est
|
||||
ajouté au pack pour ces projets.
|
||||
|
||||
Artefacts locaux vérifiés, reçus dans `artifacts.json` :
|
||||
|
||||
| Fichier dans `build/` | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `Sanctuary-beta.292.mrpack` | 16 753 207 | `28980cec018fc81f700cb8ecb3981199537e04ff737bf58b220ce442b7e5dc56` |
|
||||
| `Sanctuary-Prism-beta.292.zip` | 3 928 525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
|
||||
| `Sanctuary-Server-beta.292.zip` | 4 036 530 | `0ac362bbfbfd2c03efdaf6b209d9319e648ad546bee074521038fffc86bb640a` |
|
||||
|
||||
JAR Sanctuary : `0163da08897de96559ab51f482363bd00c03cfb3309784cdb6e112acf25a4862`.
|
||||
Le ZIP Prism d'amorçage a les mêmes octets qu'en beta.291 : il contient l'outil,
|
||||
le logo et l'adresse stable, et les versions de Minecraft/Loader n'ont pas changé.
|
||||
Le MRpack et les métadonnées du canal préparé contiennent les nouveaux mods.
|
||||
@@ -0,0 +1,109 @@
|
||||
# PACK-LOGO — logo Sanctuary pendant packwiz
|
||||
|
||||
Branche `codex/packwiz-sanctuary-logo`, préparation locale **beta.291**, depuis
|
||||
le socle beta.290. Demande du 26 octobre 2026 : afficher le bandeau Sanctuary
|
||||
fourni dans la fenêtre de téléchargement et de mise à jour, avant Minecraft.
|
||||
|
||||
## Résultat
|
||||
|
||||
Le PNG `minecraft.png` fourni par le créateur est conservé octet pour octet
|
||||
dans `launcher/packwiz/sanctuary.png` (1024 × 256, RGBA). Son SHA-256 est
|
||||
`efee73a6c812c89b2900d41f6446c0f906ccf85136b6c226daf42870a41a5157`.
|
||||
La fenêtre l'affiche sur fond noir à 640 × 160, proportions conservées,
|
||||
au-dessus de la progression réelle. Ses boutons, le texte initial et le
|
||||
compte à rebours suivent la langue du système : français, anglais sinon.
|
||||
Les messages et dialogues du moteur amont conservent leurs traductions amont.
|
||||
|
||||
`scripts/packwiz_installer.py` télécharge le JAR officiel
|
||||
[packwiz-installer v0.5.14](https://github.com/packwiz/packwiz-installer/releases/tag/v0.5.14),
|
||||
vérifie son SHA-256, puis remplace uniquement `InstallWindow.class` par
|
||||
l'adaptation Java de sa fenêtre Swing. Toutes les autres classes et notices
|
||||
restent identiques : téléchargement, vérification des hashes, sélection des
|
||||
mods facultatifs, erreurs et annulation continuent de dépendre de packwiz.
|
||||
La licence MIT de comp500 et le source de l'adaptation sont joints au JAR.
|
||||
Le logo est embarqué : aucun téléchargement d'image n'est requis pour l'afficher.
|
||||
|
||||
Le build utilise Java 25 ; les classes de la fenêtre ciblent Java 8 comme
|
||||
l'amont. Les SHA-256 de l'amont, du logo et du résultat sont fixés dans
|
||||
`launcher/packwiz/installer.json`. Une compilation différente échoue avant
|
||||
d'écrire le JAR de distribution. Les dépendances et JAR générés restent dans
|
||||
`build/packwiz-branding/`, ignoré par Git.
|
||||
|
||||
## Archive Prism et mises à jour
|
||||
|
||||
La commande habituelle `python3.14 scripts/pack.py prism <url-du-canal>`
|
||||
embarque le bootstrap officiel vérifié et le nouvel installateur vérifié,
|
||||
avec la même configuration Minecraft 26.3 / Fabric 0.19.5 et le même canal
|
||||
stable. `--bootstrap-no-update` empêche le bootstrap de remplacer la fenêtre
|
||||
Sanctuary par le binaire officiel. Cette option concerne **l'installateur** ;
|
||||
la synchronisation des mods packwiz reste active à chaque lancement.
|
||||
Le nom beta.291 du ZIP local n'avance pas ce canal : avant publication, un
|
||||
lancement visant l'adresse stable récupère la version déjà publiée, beta.289
|
||||
au moment de cette préparation. Les essais beta.291 utilisent un serveur HTTP
|
||||
de développement isolé, jamais le monde ni le serveur personnel.
|
||||
|
||||
Les évolutions de l'installateur doivent donc être préparées et testées
|
||||
explicitement, avec mise à jour du verrou et du vérificateur Web. Une simple
|
||||
avance du canal de mods ne met pas à jour l'outil d'une instance existante.
|
||||
Lors d'une future synchronisation locale demandée, sauvegarder l'instance,
|
||||
copier le JAR vérifié et ajouter l'option à sa commande avant lancement en
|
||||
conservant les autres réglages ; conserver cette même instance, sans réimport.
|
||||
Le kit serveur conserve son installateur officiel et son fonctionnement sans
|
||||
interface. Le logo n'est pas ajouté à l'index des fichiers du monde ou du jeu.
|
||||
|
||||
`verify_prism` contrôle désormais les deux JAR par leurs hashes et la présence
|
||||
de l'option qui conserve l'habillage. Les sauvegardes et fichiers personnels
|
||||
restent refusés dans le ZIP. Le MRpack n'embarque pas les commandes Prism et
|
||||
ne suffit pas à installer cet habillage.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- Logo fourni identique octet pour octet ; JAR résultant contrôlé par son
|
||||
SHA-256 `9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875`.
|
||||
- Les deux tests de distribution de `test_packwiz_installer.py` passent :
|
||||
toutes les entrées de l'amont sont relues, seule la classe de fenêtre change,
|
||||
et le véritable JAR vérifié est retrouvé dans l'export Prism.
|
||||
- Les dix tests de `test_publish_web_pack.py` passent : outils modifiés refusés,
|
||||
option de conservation obligatoire, contrôles de publication préservés.
|
||||
- Parcours Swing réel en FR et EN réussi avec le `GUIHandler` officiel :
|
||||
progression déterminée/indéterminée, options, Annuler, Continuer et compte à
|
||||
rebours. Les captures `preview-fr.png` et `preview-en.png` sont inspectées.
|
||||
Le test ne lance ni Minecraft ni un microphone.
|
||||
- Deux exécutions de `check build assemblePack` avec le profil
|
||||
`whale290,recipes,progression` échouent sur le même test serveur hérité :
|
||||
`eggQuotaNaturalSpawnAndFlightUseActualServerEntities`, quota après utilisation
|
||||
de l'œuf entre Overworld et Nether (18 réussites sur 19). Le profil isolé
|
||||
`whale290` échoue également sur ce cas (6 réussites sur 7). Les sources du
|
||||
comportement et de son test ne sont pas modifiées dans ce chantier. Cet échec
|
||||
reste ouvert ; aucune validation complète des baleines n'est revendiquée.
|
||||
|
||||
- `./gradlew check build assemblePack -PsanctuaryFocusedTests=recipes,progression
|
||||
-PsanctuaryAtlasOnly=true` réussit en **2 min 42 s**, avec **13 tests serveur
|
||||
requis réussis**, les contrôles purs de `check`, le JAR et les profils client
|
||||
et serveur assemblés. Ce périmètre ne valide pas le test de baleines en échec.
|
||||
- Comparaison des JAR beta.290/beta.291 : **2 658 classes de jeu identiques**,
|
||||
aucune classe ajoutée. Le compteur et les notices évoluent, sans changement
|
||||
du code de jeu.
|
||||
- Deux lancements du véritable bootstrap et de l'installateur adapté réussis
|
||||
depuis le ZIP extrait dans `native instance with spaces/`, avec les arguments
|
||||
de sa commande Prism et un canal HTTP local beta.291. Un seul JAR Sanctuary,
|
||||
fichiers gérés identiques au second passage, installateur avec logo conservé,
|
||||
trois fichiers témoins personnels inchangés. Aucun monde ouvert. Le mode
|
||||
sans interface vérifie la synchronisation ; les tests Swing ci-dessus
|
||||
vérifient la fenêtre. Reçu `native-validation.json`.
|
||||
|
||||
Archive locale `build/Sanctuary-Prism-beta.291.zip` : **3 928 525 octets**,
|
||||
SHA-256 `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9`.
|
||||
JAR Sanctuary beta.291 :
|
||||
`bd130418a05f820dffd86b00595c5b7ce96293ec82867926f5493a95736aee73`.
|
||||
Les interfaces Linux/Windows, le lancement Minecraft depuis Prism et une
|
||||
mise à jour de l'instance personnelle ne sont pas revendiqués par ces essais
|
||||
macOS. Le test de quota des baleines reste à résoudre avant une validation
|
||||
complète de cette base de jeu.
|
||||
|
||||
Les reçus et captures restent sous `build/packwiz-branding/`.
|
||||
|
||||
Les mods évoqués pendant ce chantier sont seulement
|
||||
[préparés en tickets pour la passe suivante](integrations-mods-next.md).
|
||||
Aucune publication du canal, du site, du serveur Beta ou de l'instance
|
||||
personnelle n'est demandée ou effectuée par cette préparation locale.
|
||||
+42
-5
@@ -4,6 +4,38 @@ Le [téléchargement direct de Sanctuary Web](beta-download.md) donne une instan
|
||||
Prism déjà configurée. Le premier lancement télécharge les mods ; les suivants
|
||||
suivent les publications explicitement demandées via Codex.
|
||||
|
||||
La [procédure de reprise et de publication complète](updates.md) relie aussi
|
||||
le serveur dédié, le téléchargement du site et l'instance locale. Les reçus
|
||||
des anciennes versions ci-dessous restent historiques.
|
||||
|
||||
## beta.293 — livraison complète publiée
|
||||
|
||||
[Reçu de livraison](release-beta293.md) : Happy Creepers de la branche dédiée,
|
||||
baleines, logo et quatre mods facultatifs réunis. Release immuable, canal
|
||||
client/serveur, CT 350, téléchargement Web et même instance Prism personnelle
|
||||
mis à jour. Import neuf et instance existante vérifiés sur deux passages,
|
||||
sauvegardes et réglages conservés. Voix UDP 25565 annoncée sur le port public
|
||||
25567 via la règle Beta existante ; ping applicatif réussi depuis le LAN.
|
||||
L'audio à deux joueurs et l'accès depuis un réseau extérieur restent à essayer.
|
||||
|
||||
## beta.292 — quatre mods facultatifs (préparation locale)
|
||||
|
||||
[Voix, cache de chunks et optimisations](optional-mods-beta292.md) : Simple
|
||||
Voice Chat 2.6.24, Bobby 5.2.16, Lithium 0.26.2 et Entity Culling 1.11.3, tous
|
||||
épinglés pour Minecraft 26.3 / Fabric. Les quatre sont facultatifs côté client ;
|
||||
Lithium et la voix sont aussi présents dans le profil serveur. Le logo de
|
||||
beta.291 et les préférences de sélection sont conservés. Préparation locale,
|
||||
sans avance du canal public ni modification d'installation personnelle.
|
||||
|
||||
## beta.291 — logo dans l'installateur (préparation locale)
|
||||
|
||||
Le [bandeau Sanctuary fourni](packwiz-logo-beta291.md) est embarqué dans la
|
||||
fenêtre packwiz du ZIP Prism préparé. Le bootstrap reste officiel ; la fenêtre
|
||||
est adaptée et l'installateur épinglé avec `--bootstrap-no-update`. Les mods
|
||||
continuent de suivre le canal stable. Les instances déjà installées devront
|
||||
recevoir ce JAR et cette option lors d'une synchronisation explicitement demandée,
|
||||
en conservant la même instance. Cette préparation ne publie pas le canal.
|
||||
|
||||
## beta.280 — JEI embarqué et profil serveur
|
||||
|
||||
JEI rejoint le JAR Sanctuary ; `mods/sanctuary.pw.toml` reste le seul artefact
|
||||
@@ -2111,18 +2143,18 @@ La procédure complète, également utilisable manuellement :
|
||||
1. Incrémenter ensemble les versions de `gradle.properties` et `packwiz/pack.toml`
|
||||
selon la [convention `beta.xxx`](versioning.md).
|
||||
2. Exécuter `./gradlew check build assemblePack` et les vérifications du ticket.
|
||||
3. Préparer les métadonnées de la livraison, avec les deux URL exactes :
|
||||
3. Pour préparer les métadonnées manuellement depuis beta.280, utiliser la
|
||||
seule URL du JAR Sanctuary contenant JEI :
|
||||
|
||||
```sh
|
||||
python3 scripts/pack.py release \
|
||||
https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.257/sanctuary-beta.257.jar \
|
||||
https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.257/jei-sanctuary-26.3-30.32.0-sanctuary.4.jar
|
||||
https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.xxx/sanctuary-beta.xxx.jar
|
||||
```
|
||||
|
||||
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
|
||||
commit et joindre les JAR Sanctuary et JEI construits, sans remplacer les artefacts d'une release
|
||||
commit et joindre le JAR Sanctuary construit, sans remplacer les artefacts d'une release
|
||||
antérieure. Vérifier que le téléchargement public possède le SHA-256 indiqué
|
||||
dans `build/packwiz-release/mods/sanctuary.pw.toml` et `jei-sanctuary.pw.toml`.
|
||||
dans `build/packwiz-release/mods/sanctuary.pw.toml`, identique au profil serveur.
|
||||
5. Publier le contenu de `build/packwiz-release/` à la racine de la branche
|
||||
`packwiz` dans un seul commit, puis vérifier les fichiers et hashes depuis
|
||||
l'URL publique. Préserver l'historique de ce canal et ne jamais y copier le
|
||||
@@ -2162,6 +2194,11 @@ déjà branchée sur le canal stable n'a pas besoin d'être réimportée.
|
||||
|
||||
## Données locales et mondes
|
||||
|
||||
Depuis beta.295, le premier lancement du client ajoute le serveur Sanctuary
|
||||
dans les favoris sans remplacer leur liste. Un marqueur local respecte ensuite
|
||||
les suppressions volontaires ; le profil serveur distribue le logo 64 × 64.
|
||||
Voir [le contrat](default-server-beta295.md).
|
||||
|
||||
Le pack livré possède uniquement les fichiers déclarés dans son index. Il ne
|
||||
déclare ni `saves/`, ni `options.txt`, `servers.dat`, `screenshots/`, ni les packs
|
||||
graphiques personnels. L'installation de packwiz et une mise à jour des mods
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Prochains tickets — carte immersive, progression et New Game+
|
||||
|
||||
**Ajout du 26 octobre 2026 :** [prochaine passe d'intégrations du pack](integrations-mods-next.md)
|
||||
pour Simple Voice Chat, Bobby facultatif, Lithium et Entity Culling, puis les
|
||||
contrats des futurs modules personnels Dozei Stream et Only Fun. Tickets
|
||||
initialement documentaires ; les quatre mods du pack sont désormais préparés
|
||||
dans [beta.292](optional-mods-beta292.md). Le cadrage historique ci-dessous est conservé.
|
||||
|
||||
Cadrage du 13 septembre 2026, après l'essai de beta.006.
|
||||
Branche documentaire : `codex/roadmap-progression-ngplus`.
|
||||
**Statut : MAP-03 implémenté en beta.007, PROG-02A en beta.008, PROG-02B en beta.009, PROG-02C en beta.010 ; CYCLE-01, PROG-03, FACTION-01 et NGP-01 sont réunis dans beta.011.**
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# CLAIM-03 — Une seule commande de propriété
|
||||
|
||||
Ticket local du **26 octobre 2026**, branche `codex/property-command`,
|
||||
préparation locale `beta.294`.
|
||||
|
||||
## Demande et résultat attendu
|
||||
|
||||
Les trois variantes de propriété encombrent les commandes proposées au joueur.
|
||||
Conserver uniquement le nom anglais `/sanctuary property`, cohérent avec les
|
||||
autres commandes. Retirer les alias `propriete` et `propriété`, y compris pour
|
||||
les duels, afin qu'aucun enregistrement secondaire ne les réintroduise.
|
||||
|
||||
Commandes conservées :
|
||||
|
||||
- `/sanctuary property` : ouvrir la carte de propriété ;
|
||||
- `/sanctuary property claim` : revendiquer le chunk courant ;
|
||||
- `/sanctuary property protect` : acheter sa protection ;
|
||||
- `/sanctuary property duel <id>` : consulter le duel proposé.
|
||||
|
||||
Ce ticket remplace uniquement les alias de commande introduits en beta.270.
|
||||
Les règles de revendication, prix, protections, interfaces FR/EN et sauvegardes
|
||||
restent identiques. Aucun contrat de migration n'est nécessaire.
|
||||
|
||||
## Critères d'acceptation
|
||||
|
||||
- La complétion de `/sanctuary prop` propose uniquement `property`.
|
||||
- Les deux alias retirés sont absents de l'arbre serveur et sont refusés,
|
||||
seuls comme avec `claim`, `protect` ou `duel`.
|
||||
- La branche anglaise et ses trois sous-commandes restent accessibles sans
|
||||
permission opérateur. Revendication et protection fonctionnent via le
|
||||
répartiteur réel des commandes.
|
||||
- Les tests de propriétés et de duels passent ; le mod et le pack portent
|
||||
ensemble `beta.294`.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`check build assemblePack` réussit en **4 min 4 s**, sur Minecraft 26.3,
|
||||
Fabric 0.19.5 et Java 25. Les **18 GameTests serveur ciblés** passent :
|
||||
propriétés historiques, claims par chunks, priorité Demeure et duels Battle.
|
||||
Le contrôle de commande vérifie la complétion unique, les anciens alias refusés
|
||||
et les sous-commandes conservées. Le scénario natif de minage revendique et
|
||||
protège désormais son terrain via les vraies commandes `property claim` et
|
||||
`property protect`, en contrôlant le propriétaire et les huit saphirs débités.
|
||||
Les **63 contrôles du registre de claims**, les **26 contrôles du journal de
|
||||
duels** et les autres vérifications de `check` passent également.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=property270,claims272,claims277,claims278 \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
```
|
||||
|
||||
Journal ignoré : `build/property294-check-build-pack.log`.
|
||||
JAR local : `mods/sanctuary/build/libs/sanctuary-beta.294.jar`.
|
||||
Les packs client et serveur sont assemblés et vérifiés dans `build/packwiz`
|
||||
et `build/packwiz-server`. La validation porte sur l'arbre serveur et les
|
||||
commandes exécutées ; aucun essai graphique n'est revendiqué pour ce ticket.
|
||||
|
||||
Préparation locale uniquement ; aucune publication ni synchronisation d'une
|
||||
installation de jeu ou du serveur Beta demandée pour ce ticket.
|
||||
@@ -0,0 +1,95 @@
|
||||
# RELEASE-289 — zombies, lumière portée et mise à jour complète
|
||||
|
||||
Branche `codex/release-beta289`. Publication demandée le 26 octobre 2026.
|
||||
État : **publiée et installée le 26 octobre 2026** sur la Beta dédiée,
|
||||
le site et l’instance Prism existante.
|
||||
|
||||
## Contenu
|
||||
|
||||
- Six infectés : Infecté, Charger, Spitter, Boomer, Smoker et Hunter, leurs œufs
|
||||
et leurs familiers adultes miniatures à taille variable, sans infecté bébé.
|
||||
- Neuf tenues fournies par le créateur, choisies par individu et conservées au
|
||||
rechargement ; zombie classique compris. Bras natifs en avant pour tous,
|
||||
Hunter accroupi au sol et bond offensif esquivable.
|
||||
- Correction des lumières portées avec Sodium, reprise du commit `6f44214`.
|
||||
- Réintégration du conditionnement JEI et de la chaîne client/serveur déjà
|
||||
publiée en beta.280, puis des outils de publication Web/Prism du runbook
|
||||
`fb20160e`. Aucun retour aux anciens réglages de carte de cette branche :
|
||||
le dézoom et les relevés de beta.279 restent conservés.
|
||||
|
||||
Le binaire local beta.288 reste immuable. La livraison beta.289
|
||||
contient aussi JEI, comme la Beta dédiée précédente. Minecraft 26.3,
|
||||
Fabric Loader 0.19.5, Fabric API 0.160.5+26.3, JEI 30.32.0-sanctuary.4,
|
||||
Sodium 0.9.2+mc26.3 et Sound Physics 1.5.1+26.3 restent épinglés.
|
||||
|
||||
## Cibles autorisées
|
||||
|
||||
Commit/push des sources, release Gitea immuable, canal packwiz stable client
|
||||
et serveur, CT Beta 350, téléchargement Web CT 320, puis synchronisation de
|
||||
la même instance personnelle Sanctuary Beta. Arrêt propre et sauvegarde de
|
||||
la Beta avant avancement du canal. Alpha CT 300, comptes, permissions,
|
||||
configuration privée, mondes et paramètres de génération conservés.
|
||||
Le site reçoit la publication de distribution ; aucun checkout live global
|
||||
n’est remplacé. Le bouton Télécharger reste le ZIP Prism prêt.
|
||||
|
||||
## Vérification et reçu
|
||||
|
||||
`./gradlew check build assemblePack assembleServerPack
|
||||
-PsanctuaryFocusedTests=recipes,progression,lights,hunter286
|
||||
-PsanctuaryAtlasOnly=true` réussit en **3 min 21 s**, avec **31 tests serveur**
|
||||
et les contrôles de la chaîne serveur/Web. Le parcours des zombies/familiers
|
||||
est rejoué sous Vulkan avec Sodium et beta.289 : réussi en 51 s.
|
||||
|
||||
Les 2 650 classes Sanctuary de beta.288 sont identiques ; le nouveau JAR
|
||||
ajoute le JEI embarqué et ses métadonnées/notices. Un seul JEI exact, côté
|
||||
commun ; aucune classe de test, aucun JEI séparé dans le MRpack. ZIP Prism prêt,
|
||||
kit serveur et archive MRpack contrôlés ; leurs contenus restent hors Git.
|
||||
Les hashes et les journaux se trouvent dans `build/release289/`.
|
||||
Les preuves propres à beta.288 restent dans `docs/dynamic-hand-light-beta288.md`.
|
||||
Elles ne sont pas présentées comme une validation du nouveau conditionnement.
|
||||
|
||||
## Publication et installation effectives
|
||||
|
||||
- Sources et tag `beta.289` : `6821f6ee12095583663f1aafb02c92fbb105683e`,
|
||||
poussés depuis `codex/release-beta289`.
|
||||
- [Release immuable](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.289)
|
||||
et trois archives publiques relues avec leurs SHA-256 exacts, HTTP 200.
|
||||
- Canal packwiz client et serveur :
|
||||
`56e3462a39d6b911c27ec926780ba5eb509e599c`, version `beta.289`.
|
||||
- CT 350 : même service `sanctuary-beta.service`, actif, JAR beta.289 et JEI
|
||||
embarqué confirmés dans le démarrage ; RCON répond, 0/20 joueurs au contrôle.
|
||||
Les 239 fichiers protégés et l’environnement privé sont identiques entre
|
||||
l’arrêt propre et la mise à jour, avant redémarrage.
|
||||
- CT 320 : bascule atomique de la distribution vers
|
||||
`storage/app/distribution/releases/beta.289/prism`. Le code live n’a pas été
|
||||
remplacé. Footer et attribut de survol du bouton public annoncent beta.289.
|
||||
Le ZIP anonyme répond HTTP 200, nom `Sanctuary-Beta.zip`, et le lien MRpack
|
||||
historique répond aussi avec le hash attendu.
|
||||
- Même communauté des deux côtés, métriques fraîches et serveur en marche.
|
||||
L’API d’admission répond 422 à un code volontairement invalide, sans appel
|
||||
de consommation d’un code.
|
||||
- Même instance personnelle **Sanctuary Beta**, dossier historique
|
||||
`Sanctuary-0.1.0-alpha.1` : passage de beta.144 à beta.289, un seul JAR
|
||||
Sanctuary, deux synchronisations identiques. Les 999 fichiers personnels
|
||||
contrôlés et les composants Minecraft/Fabric sont conservés. Aucun monde
|
||||
personnel n’a été ouvert ; l’installation isolée a aussi passé deux cycles.
|
||||
|
||||
Sauvegarde serveur complète avant avancement du canal :
|
||||
`/root/sanctuary-beta-backups/before-beta289-20261026T002155Z`,
|
||||
archive de 167 964 197 octets, SHA-256
|
||||
`724533e56bf4ab5319bbd519d123c6731fc882f684cfc0998d876c34ed09805d`.
|
||||
Sauvegarde locale complète de l’instance avant synchronisation :
|
||||
`sanctuary-backups/before-beta289-20261026T011713/instance.tar.gz`,
|
||||
230 400 000 octets. La publication Web beta.280 reste disponible pour reprise.
|
||||
|
||||
Le ping Minecraft via `beta.sanctuary-minecraft.net` et son SRV 25567 répond
|
||||
en 26.3, 0/20, depuis le Mac sur le LAN. **L’accès depuis un réseau extérieur
|
||||
n’a pas été testé.** Les vérifications graphiques de cette livraison ciblent
|
||||
Vulkan uniquement. Reçus détaillés ignorés sous `build/release289/`.
|
||||
|
||||
## Artefacts vérifiés
|
||||
|
||||
- `Sanctuary-beta.289.mrpack` : 16,715,428 octets ; SHA-256 `7a8935225a97119ba278f8a7ccd51aae7efef588244be782661545c67270b8fc`.
|
||||
- `Sanctuary-Prism-beta.289.zip` : 92,417 octets ; SHA-256 `30dd37f3a6e1d79f8b443f6312918258276c85d5b6a5e7fbdc9c3fea7562dbf2`.
|
||||
- `Sanctuary-Server-beta.289.zip` : 4,036,416 octets ; SHA-256 `5b86b16e9d0fd944363016b79df397391eda88f24dc80b9cf0e2bd2f71fe75d9`.
|
||||
- JAR Sanctuary SHA-256 : `feec13d0b0e708473179ac3234060fb58858d94d19bc9ddf9b7137512875dc41`.
|
||||
@@ -0,0 +1,136 @@
|
||||
# beta.293 — livraison intégrée
|
||||
|
||||
Livraison demandée le **26 octobre 2026** pour essayer le parcours complet :
|
||||
site → ZIP Prism → packwiz avec logo → jeu et serveur Beta.
|
||||
Branche `codex/release-beta293`.
|
||||
|
||||
## Contenu et provenance
|
||||
|
||||
La base conserve les baleines de `a93e2611`, l'habillage packwiz et les quatre
|
||||
mods facultatifs de `97c601cd`. Les Happy Creepers sont repris du commit
|
||||
`0ffd371a` de `codex/happy-creepers`, sans réimplémentation : les règles et la
|
||||
texture restent celles de cette branche dédiée. Les conflits d'intégration
|
||||
portaient sur les versions, les listes de tests et les documents communs.
|
||||
La livraison utilise le prochain compteur libre **beta.293** ; les préparations
|
||||
locales beta.291 et beta.292 ne sont pas publiées séparément.
|
||||
|
||||
[Notes de version](notes-beta293.md), [Happy Creepers](happy-creepers.md),
|
||||
[logo packwiz](packwiz-logo-beta291.md), [mods facultatifs](optional-mods-beta292.md).
|
||||
|
||||
Le test du quota interdimensionnel des baleines attend désormais la fin du
|
||||
chargement des entités du chunk Nether via `waitForEntities`. Ses deux fixtures
|
||||
restent immobiles : l'une au centre du chunk forcé, l'autre en hauteur pour que
|
||||
sa boîte de 36 blocs ne croise pas le nettoyage des tests voisins. Le test
|
||||
d'origine pouvait perdre une fixture avant de vérifier le quota. Le code de
|
||||
jeu reste identique ; **79 tests serveur** passent sur la base réunie, avec
|
||||
Lithium et la voix chargés. Le déplacement de la baleine naturelle reste testé
|
||||
avec son IA active.
|
||||
|
||||
## Cibles de livraison
|
||||
|
||||
- Artefacts immuables et canal client/serveur Gitea ;
|
||||
- CT **350** et service `sanctuary-beta.service`, après arrêt et sauvegarde ;
|
||||
- téléchargement direct de Sanctuary Web, CT **320**, sans modification du code live ;
|
||||
- même instance Prism **Sanctuary Beta**, avec sauvegarde, installateur habillé
|
||||
vérifié et conservation des données personnelles.
|
||||
|
||||
La règle NAT Beta existante `25567 → 192.168.1.122:25565` couvre TCP et UDP.
|
||||
La voix utilise **UDP 25565 dans la CT**, annoncé comme
|
||||
`beta.sanctuary-minecraft.net:25567`. Aucun changement de Bbox, UPnP, Alpha,
|
||||
RCON ou règles des autres serveurs n'est nécessaire.
|
||||
|
||||
## Validation de la livraison
|
||||
|
||||
`check build assemblePack assembleServerPack` : **réussi en 2 min 46 s**,
|
||||
avec les contrôles purs et **79 tests serveur requis**. Profil natif :
|
||||
`whale290,happycreeper,cosmetics,headblocks,recipes,progression,smoker285,hunter286`,
|
||||
`sanctuaryAtlasOnly=true`, `sanctuaryOptional292ServerTests=true`.
|
||||
Journal : `build/release293/check-build-final.log`.
|
||||
|
||||
Parcours Happy Creepers **Vulkan avec Sodium et les quatre mods chargés** :
|
||||
toutes les assertions en jeu réussissent sur deux essais, capture des quatre
|
||||
états inspectée (`build/release293/happy-four-states-vulkan.png`). Équipement et
|
||||
retrait au clavier/souris, fleur exacte synchronisée, visage restauré, IA
|
||||
passive près du joueur et charge indépendante vérifiés. La fermeture du monde
|
||||
reste bloquée dans la synchronisation `Phaser` de Fabric Client GameTest ; ce
|
||||
contrôle graphique n'est donc pas un succès de tâche Gradle. Le premier dump
|
||||
montre le thread de test dans `TestSingleplayerContextImpl.close`, le serveur
|
||||
dans `ThreadingImpl.enterPhase` et le rendu dans `IntegratedServer.halt`.
|
||||
Ce blocage de l'infrastructure de test est consigné séparément des assertions
|
||||
réussies et du build complet réussi. Les parcours des quatre mods avec et sans
|
||||
Sodium ont leur reçu dans [beta.292](optional-mods-beta292.md).
|
||||
|
||||
Les archives ont été contrôlées avant publication : sept dépendances externes
|
||||
et un seul JAR Sanctuary dans le MRpack ; huit mods du client et quatre du
|
||||
serveur ; JEI embarqué, aucun test ni dépendance facultative imposée dans le
|
||||
JAR Sanctuary. Texture happy identique à la branche dédiée, classes creeper
|
||||
et baleine présentes. ZIP Prism sans mods ni sauvegardes personnelles.
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `sanctuary-beta.293.jar` | 18171689 | `4a0a3941f651ba3566ec41940c2f4c820d917121312e3aead23b39f99633ebe3` |
|
||||
| `Sanctuary-beta.293.mrpack` | 16761300 | `80e1d9a54165f279738a6486c8c3cd7e5e90ad7ad4ca81d32a4d4a04b15f0e90` |
|
||||
| `Sanctuary-Prism-beta.293.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
|
||||
| `Sanctuary-Server-beta.293.zip` | 4036536 | `5f3135f7f5b5448023f9873c5242e3c2ab9444c197e1b551d356c8b1120bf01d` |
|
||||
|
||||
## État opérationnel
|
||||
|
||||
**Livraison complète effectuée le 26 octobre 2026.**
|
||||
|
||||
| Cible | État vérifié |
|
||||
| --- | --- |
|
||||
| Source et tag `beta.293` | `678d77fb91ef5e3ffee96cac5c88ebafa0a77976` |
|
||||
| Canal client/serveur | `687d07e57ec008e1865a0081f317c4f3e3159e14` |
|
||||
| Release Gitea | [beta.293](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.293), quatre artefacts publics vérifiés |
|
||||
| CT 350 | Service actif, Sanctuary beta.293, Fabric API, Lithium et Voice Chat chargés ; démarrage sans erreur |
|
||||
| CT 320 | Publication atomique `releases/beta.293/prism`, ancienne beta.289 conservée |
|
||||
| Prism personnel | Même instance **Sanctuary Beta**, huit mods et un seul JAR Sanctuary beta.293 ; deux synchronisations identiques |
|
||||
|
||||
Le serveur était vide (0/20) avant arrêt. Sauvegarde complète privée :
|
||||
`/root/sanctuary-beta-backups/before-beta293-20261026T032339Z/instance.tar.gz`,
|
||||
SHA-256 `b9bd9fa7b10d7e54b3595d52061529a3da8a5c9193f1854966a60fda975e9f11`.
|
||||
Les **240 fichiers protégés sont identiques après synchronisation, avant
|
||||
redémarrage**, de même que l'environnement privé. Après redémarrage, Minecraft
|
||||
réécrit le commentaire horodaté de `server.properties` ; toutes les clés et
|
||||
valeurs restent identiques. Seul le nouveau fichier vocal a été configuré :
|
||||
`port=25565`, `bind_address=` vide,
|
||||
`voice_host=beta.sanctuary-minecraft.net:25567`, `force_voice_chat=false`.
|
||||
Aucun changement de monde, de génération ou de format n'est effectué par
|
||||
l'outil de mise à jour ; le serveur reprend son monde existant.
|
||||
|
||||
La voix écoute en UDP 25565. Un ping applicatif vocal valide répond via le
|
||||
nom public et le port 25567 depuis le Mac (68 ms). Le statut Minecraft via
|
||||
DNS/SRV répond également : 26.3, 0/20. Ces contrôles partent du LAN via
|
||||
l'adresse publique : **l'accès depuis un autre réseau et l'audio audible à
|
||||
deux joueurs restent à essayer**. L'authentification vocale dans les mondes de
|
||||
test est réussie ; elle ne remplace pas cet essai vocal distant.
|
||||
L'API d'admission authentifiée refuse le code volontairement invalide
|
||||
(`HTTP 200`, `valid=false`, `reason=invalid`), sans appel à `redeem`.
|
||||
Les métriques Web sont fraîches, en marche, avec le même UUID communautaire
|
||||
`cc6f4f5a-e58d-409a-879f-81daecf59461` et la même base de jeu.
|
||||
|
||||
Le [ZIP direct du site](https://sanctuary-minecraft.net/downloads/sanctuary-prism.zip)
|
||||
répond HTTP 200 sous le nom **Sanctuary-Beta.zip**, avec le hash du tableau.
|
||||
Le lien MRpack historique répond aussi HTTP 200 avec son hash exact.
|
||||
Le bouton, son survol et le footer affichent beta.293. Le code Web live est
|
||||
resté sur `eadc30e8c7b6dda50bc810584af057235e7fdba8` ; les **73 entrées Git
|
||||
préexistantes sont identiques**, sans pull, reset ni reconstruction de cache.
|
||||
Une importation isolée du ZIP a récupéré les huit mods depuis le vrai canal
|
||||
HTTPS, puis une deuxième synchronisation a conservé exactement leurs hashes.
|
||||
|
||||
Sauvegarde Prism complète sous le dossier historique de l'instance :
|
||||
`sanctuary-backups/before-beta293-20261026T030401Z/instance.tar.gz`, SHA-256
|
||||
`9bfc9b963551885d3e124c865aabe0dc9750ecb6cfed89b7d936a3e14abe51bb`.
|
||||
Les **999 fichiers personnels contrôlés sont inchangés**, et aucun monde
|
||||
personnel n'a été ouvert. Seules les deux commandes `PreLaunchCommand` de
|
||||
`instance.cfg` et le JAR de l'installateur ont reçu l'habillage ; les autres
|
||||
lignes de configuration sont identiques. L'outil reste épinglé par
|
||||
`--bootstrap-no-update`, et le pack continue de suivre le canal stable.
|
||||
Sodium et Sound Physics ont gardé leurs sélections ; les quatre nouveaux
|
||||
choix sont activés par défaut et restent facultatifs côté client.
|
||||
Installateur SHA-256 :
|
||||
`9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875`.
|
||||
|
||||
Les journaux et reçus détaillés sont sous `build/release293/`, hors de Git.
|
||||
Le worktree original `codex/happy-creepers` est resté propre au commit
|
||||
`0ffd371adac0c4f950237448625673b83a94bee5`.
|
||||
@@ -0,0 +1,80 @@
|
||||
# beta.295 — livraison du serveur préenregistré
|
||||
|
||||
Livraison complète du **26 octobre 2026**, branche `codex/default-server`.
|
||||
[Contrat PACK-03](default-server-beta295.md), [notes de version](notes-beta295.md).
|
||||
Le ticket CLAIM-03 terminé est intégré depuis ses fichiers validés, sans
|
||||
modifier son checkout. beta.294 reste une préparation locale non publiée.
|
||||
|
||||
## Versions réellement livrées
|
||||
|
||||
| Cible | État vérifié |
|
||||
| --- | --- |
|
||||
| Source et tag `beta.295` | `2734c2383a8ce8562506a9d6bd1da036084143a5` |
|
||||
| Canal client/serveur | `9a3dd1bea975efc94cbdd5e9f7ef7288458063b7` |
|
||||
| Release | [beta.295](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.295), quatre artefacts publics vérifiés |
|
||||
| CT 350 | Service actif, quatre mods dont Sanctuary beta.295, démarrage sans erreur |
|
||||
| Icône annoncée | Logo PNG 64 × 64, hash du ping public identique à la ressource du JAR |
|
||||
| CT 320 | `releases/beta.295/prism`, beta.293 conservée, téléchargement direct et footer actualisés |
|
||||
| Prism historique | Même instance Sanctuary Beta, huit mods et un seul JAR Sanctuary beta.295 |
|
||||
|
||||
Le ZIP du site reste une amorce du canal stable. Ses octets sont identiques
|
||||
à ceux de beta.293 car la plateforme et les outils sont inchangés ; le premier
|
||||
lancement récupère désormais le JAR beta.295 qui ajoute le serveur.
|
||||
|
||||
## Vérifications
|
||||
|
||||
- `check build assemblePack assembleServerPack` réussi en **3 min 22 s** :
|
||||
contrôles purs et **18 tests serveur** `property270,claims272,claims277,claims278`.
|
||||
- **15 contrôles des favoris** : conservation, sauvegarde, absence de doublon,
|
||||
suppression volontaire, lecture vanilla de l’icône et corruption réparable.
|
||||
- **Vulkan FR/EN**, GUI 3 en 1280 × 800 : réussi en 38 s, captures inspectées.
|
||||
Hook réel de démarrage et liste multijoueur vanilla ; aucun monde ouvert.
|
||||
- Deux synchronisations packwiz identiques dans une importation isolée, puis
|
||||
deux dans l’instance historique. Serveur ajouté dans celle-ci avec le même
|
||||
code d’initialisation, deux appels identiques, sans lancer le jeu.
|
||||
- Ping public via DNS/SRV : Minecraft 26.3, 0/20, icône vérifiée depuis le LAN.
|
||||
Aucun essai depuis un réseau extérieur ajouté.
|
||||
- Admission : HTTP 200, code volontairement invalide refusé, aucun redeem.
|
||||
Métriques Web fraîches et en marche ; même base et UUID communautaire.
|
||||
|
||||
## Artefacts
|
||||
|
||||
| Artefact | Octets | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `sanctuary-beta.295.jar` | 18179585 | `45a4af94c56bdd25f1eeeaa5e642588e0f9b1ab3f77e934f396fee1a73898831` |
|
||||
| `Sanctuary-beta.295.mrpack` | 16768587 | `ab1b71de5a70b0f097a2b89a5ed7d4ca2ca3b3453b19d404b5ebf85ff205cfd7` |
|
||||
| `Sanctuary-Prism-beta.295.zip` | 3928525 | `95c57f5e70627a2847d1cc2033c8d9f50f44ef4a302ca892bc727e0c37ece0a9` |
|
||||
| `Sanctuary-Server-beta.295.zip` | 4036670 | `40841bf0f0c8a16d56ef04441f0f71729348b14e6eba98ebf0865aacea843b67` |
|
||||
|
||||
Icône serveur SHA-256 :
|
||||
`2b8966e8f2a0cfe064a1dfac4205874216373f8bb4c59e8eb712d5e7d204fce7`.
|
||||
|
||||
## Conservation et limites
|
||||
|
||||
Serveur vide avant arrêt. Sauvegarde complète privée :
|
||||
`/root/sanctuary-beta-backups/before-beta295-20261026T052413Z/instance.tar.gz`,
|
||||
SHA-256 `b46c036a439944f79d5f89a188064e7507bc5804ffaf8e3a6f42458319340f66`.
|
||||
Les **248 fichiers protégés** et l’environnement privé sont identiques après
|
||||
synchronisation, avant redémarrage. Ensuite toutes les clés/valeurs de
|
||||
`server.properties` restent identiques ; Minecraft réécrit son commentaire
|
||||
horodaté. Voix, réseau et monde existant gardent leurs réglages. Aucune
|
||||
migration de monde ou de génération.
|
||||
|
||||
Prism : sauvegarde complète sous le dossier de l’instance historique,
|
||||
`sanctuary-backups/before-beta295-20261026T052117Z/instance.tar.gz`,
|
||||
SHA-256 `a128a1463cf598707674c84834c9b456c36d781fbf558daa4ce195713ceca8ca`.
|
||||
Les **999 fichiers personnels préexistants** et `instance.cfg` sont identiques.
|
||||
Seuls les fichiers gérés sont actualisés, et les nouveaux `servers.dat` et
|
||||
`.sanctuary/default-server-beta` initialisent le favori. Aucun monde personnel ouvert.
|
||||
|
||||
Une autre instance importée, dossier `Sanctuary-Beta`, était en cours de jeu.
|
||||
Elle n’a pas été modifiée pendant cette session et suivra le canal beta.295
|
||||
au prochain lancement. Aucune nouvelle instance personnelle créée ici.
|
||||
|
||||
Web : HEAD `eadc30e8c7b6dda50bc810584af057235e7fdba8` et **73 changements préexistants** identiques.
|
||||
Aucun remplacement du code live ni reconstruction de cache. ZIP et MRpack
|
||||
publics HTTP 200 avec hashes exacts, ZIP reçu sous le nom `Sanctuary-Beta.zip`.
|
||||
|
||||
Reçus, journaux, hashes et scripts opérationnels : `build/release295/`, hors Git.
|
||||
Les limites vocales antérieures restent celles de [beta.293](release-beta293.md) ;
|
||||
cette livraison ne revendique pas de nouvel échange audible à deux joueurs.
|
||||
+9
-2
@@ -8,7 +8,7 @@ Plateforme épinglée : Minecraft **26.3**, Fabric Loader **0.19.5**, Fabric API
|
||||
## Préparer le kit
|
||||
|
||||
Dans les sources : `./gradlew check build assemblePack assembleServerPack`.
|
||||
Le kit est `build/Sanctuary-Server-beta.280.zip` ; les manifestes locaux sont
|
||||
Le kit de la livraison courante est `build/Sanctuary-Server-beta.293.zip` ; les manifestes locaux sont
|
||||
dans `build/packwiz-server/`. Le ZIP est un amorçage : il télécharge les mods
|
||||
depuis le canal au premier démarrage, il n'est pas une copie hors ligne du pack.
|
||||
Il n'est utilisable sur le canal public qu'après publication de cette version.
|
||||
@@ -56,6 +56,10 @@ avant de rendre le verrou de mise à jour.
|
||||
|
||||
L'API d'accueil et la communauté sont deux services existants distincts.
|
||||
Le pack ne contient ni secret ni configuration de production.
|
||||
Depuis beta.295, le profil serveur gère aussi `server-icon.png`, le logo du
|
||||
pack en 64 × 64. La synchronisation le pose à la racine ; Minecraft l'annonce
|
||||
aux clients au démarrage suivant. Le favori client est ajouté séparément au
|
||||
premier lancement, sans indexer le fichier personnel `servers.dat`.
|
||||
|
||||
Pour l'accès/candidature, fournir dans l'environnement du service Minecraft :
|
||||
|
||||
@@ -86,7 +90,10 @@ Le kit n'effectue aucune migration SQL ni modification de sauvegarde.
|
||||
|
||||
Arrêter le service, sauvegarder, lancer `./server.sh update`, vérifier les logs,
|
||||
puis redémarrer le même dossier serveur. `packwiz.json` doit rester présent.
|
||||
Le pack ne possède que les mods Fabric API et Sanctuary. Il ne gère ni monde,
|
||||
Depuis la préparation beta.292, le pack serveur possède Fabric API, Sanctuary,
|
||||
Lithium et Simple Voice Chat. Bobby et Entity Culling restent uniquement
|
||||
clients. [Versions, validation et port UDP vocal](optional-mods-beta292.md).
|
||||
Il ne gère ni monde,
|
||||
`server.properties`, `eula.txt`, `config/`, listes de joueurs, permissions,
|
||||
captures ou secrets. Les réglages des opérateurs restent en place.
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# ZOMBIES-285 — Smoker, lasso et traction en tyrolienne
|
||||
|
||||
Branche `codex/smoker`, sur le socle local beta.284 conservé. État : implémenté et vérifié localement en beta.285.
|
||||
|
||||
## Contrat demandé
|
||||
|
||||
Le Smoker doit toucher réellement sa cible avec un lasso avant de la ramener
|
||||
vers lui par un trajet de tyrolienne. Le projectile garde sa direction de tir :
|
||||
une esquive pendant le vol, un obstacle ou un tir trop court peut le faire rater.
|
||||
|
||||
- Modèle adulte du zombie vanilla 26.3, t-shirt gris clair et pantalon vert
|
||||
olive recolorés par script, tête/peau/chaussures/UV/alpha conservés.
|
||||
Identifiants `sanctuary:smoker` et `sanctuary:smoker_spawn_egg`. Aucun bébé,
|
||||
y compris via œuf, données chargées ou proxy du familier ; pas de noyé.
|
||||
- Sauvage : 10 PV, vitesse 0,23, mêlée 3, poursuite 36 blocs. Tir à 4–14 blocs
|
||||
avec ligne de vue, préparation de 18 ticks, lasso à 1,05 bloc/tick, recharge
|
||||
de 160 ticks engagée au lancer, même si le lasso rate.
|
||||
- Un câble visible relie le Smoker au lasso en vol, puis à la cible touchée.
|
||||
Un seul câble par source et une seule traction par victime. La source reste
|
||||
immobile. Traction à **0,58 bloc/tick**, même vitesse que les tyroliennes,
|
||||
jusqu'à environ 1,8 bloc de la source, sans pendule et sans dégâts périodiques.
|
||||
- Le volume complet de la victime est vérifié tous les 0,20 bloc : mur, bordure,
|
||||
hauteur invalide ou chunk non chargé arrêtent la traction. Aucun passage à
|
||||
travers les blocs, aucune génération de terrain ou installation de poulie.
|
||||
La gravité reprend ensuite normalement ; les chutes restent possibles.
|
||||
- Blesser la source, frapper le crochet, tuer la source, quitter la dimension,
|
||||
téléporter l'un des acteurs ou dépasser la durée coupe le câble. Le trajet
|
||||
hostile impose le déplacement après une touche ; s'accroupir ne le désactive
|
||||
pas. Les montures/passagers, créatifs/spectateurs et boss ne sont pas capturés.
|
||||
- Familier rare, adulte miniature à taille variable persistante : 12 PV,
|
||||
vitesse 0,26, lasso automatique et G à 14 blocs. Le tir doit aussi toucher ;
|
||||
dégâts d'impact et contrôle passent par les protections communes des familiers
|
||||
(propriétaire, alliés, claims, duels et résistance aux contrôles répétés).
|
||||
- Œuf créatif, apparition rare dans les habitats de zombies de l'Overworld,
|
||||
poids 4, un individu. Recharge sauvegardée ; câble et trajet transitoires non
|
||||
sauvegardés. Aucun identifiant ou format existant remplacé.
|
||||
|
||||
## Vérification
|
||||
|
||||
- **19 tests ciblés Smoker** réussis : 18 nouveaux scénarios et le contrôle
|
||||
du framework. Œuf/profil/spawn, adulte, préparation et recharge au lancer,
|
||||
contact réel préalable au déplacement, vitesse 0,58, esquive en vol, mur,
|
||||
obstacle bas sur le corps, blessure de la source même après guérison, crochet
|
||||
coupé, source retirée/déplacée, téléportation et créatif, exclusivité des
|
||||
trajets, sauvegarde sans corde/bébé, chunk non chargé, G avec dégâts/contrôle
|
||||
communs, raté sans remboursement, annulation par ordre, propriétaire/animaux/
|
||||
boss protégés, arrêt avant une cellule interdite et tailles 120 à 6000.
|
||||
- **77 régressions** réussies dans une autre session neuve : tyroliennes fixes
|
||||
et portatives, Boomer, Spitter, Charger, Infecté et socle des familiers. Les
|
||||
deux suites incluent chacune le contrôle du framework. Le mélange des fixtures
|
||||
de joueurs simulés du Smoker avec les scénarios d'IA asynchrones a provoqué
|
||||
des interférences dans un monde partagé ; les suites sont donc isolées,
|
||||
sans modifier les tests des infectés précédents. Journaux :
|
||||
`build/smoker285-check-build-pack.log` et `build/smoker285-regressions.log`.
|
||||
- `./gradlew check build assemblePack` réussi en **2 min 49 s**, avec
|
||||
`-PsanctuaryFocusedTests=smoker285 -PsanctuaryAtlasOnly=true`. Les régressions
|
||||
utilisent `ziplines242,boomer284,spitter283,charger282,infected280,familiar260,companions`
|
||||
avec le même monde plat explicite de graine `0`, dans une session distincte.
|
||||
- Parcours client natif **Vulkan / MoltenVK 1.4.2 / Apple M1**, Java 25,
|
||||
Minecraft 26.3, Loader 0.19.5, monde neuf de graine `285` : réussi avec arrêt
|
||||
normal du client. Appui réel sur G, vol puis touche et traction de l'ennemi,
|
||||
câble natif segmenté, recharge, trois tailles adultes 500/1000/1500 et
|
||||
infobulles/écran FR/EN GUI 2/3/4 vérifiés.
|
||||
- Le même parcours joue le rôle de la victime : un déplacement latéral réel
|
||||
au clavier fait rater le premier lasso. Le second touche réellement, puis
|
||||
transporte le joueur malgré Reculer/Accroupir, jusqu'au Smoker. Position du
|
||||
joueur vérifiée côté serveur et dans le client rendu, câble supprimé à
|
||||
l'arrivée, gravité normale conservée. Le familier reprend son positionnement
|
||||
de combat habituel après avoir achevé sa traction ; l'arrivée est contrôlée
|
||||
par rapport au point d'ancrage effectif du tir.
|
||||
- Texture 64×64 comparée en RGBA au zombie vanilla épinglé : **331 pixels de
|
||||
t-shirt et 239 de pantalon** recolorés, coordonnées/alpha/tête inchangés.
|
||||
Les quatre anciennes textures sont identiques. Œuf 16×16 comparé à sa
|
||||
recoloration attendue, silhouette intacte. Aucun outil de génération d'images.
|
||||
- JAR et export MRpack : ZIP intègres, version beta.285, dépendances exactes,
|
||||
classes, œuf, modèle, texture, profil, butin, tags et traductions présents.
|
||||
Classes de tests exclues ; JAR du pack identique au JAR compilé.
|
||||
Le pack beta.284 précédent est conservé octet pour octet.
|
||||
|
||||
[Pack local beta.285](../build/Sanctuary-beta.285.mrpack), **16 666 919 octets**.
|
||||
SHA-256 : `49dbe9798ecd567f6f53b0482515682b25f4a0dc2c09c24126a3d1579cacf5d0`.
|
||||
[Reçu de validation et empreintes](../build/smoker285-validation.json).
|
||||
|
||||

|
||||

|
||||
|
||||
Livraison locale uniquement : aucun tag/release, canal packwiz, serveur personnel
|
||||
ou instance Prism modifié. Les œufs restent créatifs ; leur acquisition en
|
||||
survie relève d'un autre ticket. Essais de transport sur serveur natif et
|
||||
serveur intégré ; latence de clients distants non couverte. Aucun monde
|
||||
personnel ni format de sauvegarde existant modifié.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# ZOMBIES-283 — Spitter et zone d'acide
|
||||
|
||||
Branche `codex/spitter`, sur le socle local beta.282 conservé. État : implémenté et vérifié localement en beta.283.
|
||||
|
||||
## Contrat demandé
|
||||
|
||||
Le Spitter utilise toujours le modèle adulte du zombie, un t-shirt orange et
|
||||
un pantalon bleu ardoise. Son œuf est `sanctuary:spitter_spawn_egg` et son
|
||||
familier reprend l'adulte en miniature, avec le trait de taille existant.
|
||||
Les textures sont recolorées par script à partir du PNG vanilla 26.3, avec
|
||||
peau, visage, chaussures, transparence et UV conservés.
|
||||
|
||||
**Le comportement diffère de 26.2**, selon le retour du créateur : le crachat
|
||||
ne blesse ni n'empoisonne directement. Il dépose à l'impact une **zone verte
|
||||
persistante**, utilisant les particules et le rayon d'un nuage de potion.
|
||||
Une attente de 10 ticks laisse réagir, puis la zone inflige 1 dégât par seconde
|
||||
aux entités admissibles qui restent dedans. En sortir arrête immédiatement les
|
||||
dégâts ; aucun poison ou ralentissement ne prolonge l'effet après la sortie.
|
||||
|
||||
- Spitter : 10 PV de base, vitesse 0,24, poursuite 36 blocs, mêlée 3 si la cible
|
||||
approche à moins de 3 blocs. À 3–15 blocs avec ligne de vue : préparation de
|
||||
14 ticks, crachat visible, recharge de 100 ticks.
|
||||
- Zone sauvage : rayon initial de 2,5 blocs qui diminue, durée de 100 ticks
|
||||
après l'attente, au sol ; les murs et étages séparent les victimes. Un seul
|
||||
nuage actif par source, le suivant remplace le précédent. Source et autres
|
||||
infectés de la famille immunisés contre la zone sauvage.
|
||||
- Projectile : collision native, durée et distance bornées, arrêt avant les
|
||||
chunks non chargés. Une surface solide proche de l'impact est nécessaire.
|
||||
Aucun bloc changé, aucun terrain ou chunk généré par la mécanique.
|
||||
- Œuf créatif et spawn rare dans les habitats de zombies de l'Overworld :
|
||||
poids 4, un seul individu. Aucun bébé, pas de transformation en noyé.
|
||||
- Familier rare : 12 PV, vitesse 0,28, positionnement à distance ; initiative
|
||||
de petite zone et technique G de zone persistante de 2,5 blocs / 5 secondes.
|
||||
Dégâts périodiques et destinataires passent par les règles des familiers,
|
||||
avec protection du propriétaire, des alliés et des duels. Les tirs automatiques
|
||||
attendent la fin du projectile puis de la zone active, afin de préserver G.
|
||||
- Projectiles et zones sont transitoires et ne sont pas sauvegardés. La recharge
|
||||
du mob est conservée ; aucun format existant ni identifiant livré modifié.
|
||||
|
||||
Les autres infectés et l'acquisition des œufs en survie restent hors périmètre.
|
||||
Aucun monde personnel ouvert ; publication ou mise à jour Prism non demandée.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `./gradlew check build assemblePack` réussi, avec
|
||||
`-PsanctuaryFocusedTests=spitter283,charger282,infected280,familiar260,companions`
|
||||
et `-PsanctuaryAtlasOnly=true` : **54 tests serveur**, dont **9 nouveaux**.
|
||||
Fixtures plates explicites, sans ouvrir de sauvegarde personnelle.
|
||||
- Couverture Spitter : œuf/profil/spawn, adulte et tailles persistantes, délai,
|
||||
pulses, sortie sans statut, rétrécissement/expiration, murs/étages, famille,
|
||||
remplacement et nettoyage, non-sauvegarde, surface absente, chunks non chargés,
|
||||
vraie préparation/tir sauvage, interruption/recharge conservée, vrai projectile
|
||||
de familier, propriétaire et foule de 13 animaux protégés. Les entités immunes
|
||||
sont écartées avant le budget de 12 victimes admissibles par pulse.
|
||||
- Parcours client natif **Vulkan / MoltenVK 1.4.2 / Apple M1**, graine `283`,
|
||||
Minecraft **26.3**, Fabric Loader **0.19.5**, Java **25** : réussi avec arrêt
|
||||
normal du client. Appui réel sur G, projectile/zone/dégâts, sortie sans nouveaux
|
||||
dégâts, recharge et préparation/zone sauvage vérifiés. Modèle adulte aux tailles
|
||||
500/1000/1500 et écran/infobulles FR/EN aux échelles GUI 2/3/4 contrôlés.
|
||||
- Le parcours intégré a révélé une collision prédite côté client qui retirait
|
||||
l'entrée serveur du projectile avant l'impact réel. Le suivi et la résolution
|
||||
des projectiles gérés restent désormais côté serveur ; le test G couvre ce cas.
|
||||
- Texture 64×64 : **331 pixels de t-shirt et 239 de pantalon** recolorés.
|
||||
Résultat RGBA comparé au script et au PNG natif épinglé ; visage, peau,
|
||||
chaussures, alpha et UV conservés. Empreintes des textures Infecté/Chargeur
|
||||
précédentes inchangées. Icône d'œuf historique comparée octet par octet.
|
||||
- JAR et export MRpack : ZIP intègres, version exacte beta.283, versions
|
||||
Minecraft/Loader vérifiées, classes et données Spitter présentes, tests exclus.
|
||||
Le JAR contenu dans le pack correspond exactement au JAR compilé.
|
||||
|
||||
[Pack local beta.283](../build/Sanctuary-beta.283.mrpack), **16 634 641 octets**.
|
||||
SHA-256 : `399077b7dba950550fcbfd2492859e26acc0939d45e8206673a44baa88a9a77f`.
|
||||
[Reçu de validation et empreintes](../build/spitter283-validation.json).
|
||||
|
||||

|
||||
|
||||
Les essais utilisent des mondes neufs de développement. La distribution reste
|
||||
locale : aucun tag/release, canal packwiz, serveur personnel ou instance Prism
|
||||
modifié. L'acquisition des œufs en survie et les autres infectés restent à traiter.
|
||||
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
# UPDATE-OPS — développer et publier Sanctuary
|
||||
|
||||
Procédure de reprise demandée par Koka le **26 octobre 2026**, branche
|
||||
`codex/update-workflow`. Elle relie les sources du jeu, packwiz, le serveur Beta,
|
||||
Sanctuary Web et l'instance Prism locale. Les anciennes livraisons restent dans
|
||||
[packwiz.md](packwiz.md) ; le parcours de téléchargement actuel est décrit dans
|
||||
[beta-download.md](beta-download.md).
|
||||
|
||||
## Reprendre un chantier
|
||||
|
||||
Lire `AGENTS.md`, `README.md`, le ticket et l'état Git des dépôts concernés.
|
||||
Choisir la branche source qui contient les dernières livraisons voulues, puis
|
||||
travailler sur `codex/<sujet>`. Les branches des collègues peuvent avancer : ne
|
||||
pas prendre automatiquement `main`, le plus grand compteur ou un autre worktree
|
||||
comme base. Comparer les commits et les changements avant toute intégration.
|
||||
|
||||
| Élément | Référence à retrouver |
|
||||
| --- | --- |
|
||||
| Sources du jeu | `https://git.botsu.net/koka/sanctuary-beta.git` |
|
||||
| Sources du site | `https://git.botsu.net/chris/sanctuary-web.git` |
|
||||
| Canal client | `https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml` |
|
||||
| Canal serveur | Même adresse, avec `packwiz/server/pack.toml` |
|
||||
| Site actif | `https://sanctuary-minecraft.net` — Sanctuary Web, pas un ancien site de release |
|
||||
| Entrée Minecraft | `beta.sanctuary-minecraft.net` — SRV vers le port public 25567 |
|
||||
| Téléchargement unique du site | `/downloads/sanctuary-prism.zip`, nom reçu `Sanctuary-Beta.zip` |
|
||||
|
||||
Développer/tester localement ne publie pas. Un simple `commit et push` pousse
|
||||
les sources ; il ne redémarre pas les services. Quand Koka demande une livraison
|
||||
complète ou « mets tout à jour », dérouler les cibles demandées jusqu'à leur
|
||||
vérification sans redemander les choix déjà établis. Cette note mémorise la
|
||||
procédure ; elle ne lance aucune publication périodique.
|
||||
|
||||
## Préparer une livraison du jeu
|
||||
|
||||
Pour chaque nouveau binaire Sanctuary, prendre le prochain numéro **libre**
|
||||
`beta.xxx` après examen des branches/releases. Aligner `mod_version` et
|
||||
`pack_version` dans `gradle.properties`, puis `version` dans `packwiz/pack.toml`.
|
||||
Une modification documentaire ou du site seule ne change pas ce compteur.
|
||||
|
||||
Utiliser **Java 25** et **Python 3.11+**. Sur le Mac actuel, `python3` peut encore
|
||||
être Python 3.9 : utiliser `python3.14` pour les outils opérationnels. Aucun
|
||||
changement de Minecraft/Fabric ou de format de sauvegarde n'est implicite.
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack assembleServerPack
|
||||
python3.14 scripts/pack.py prism https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
|
||||
```
|
||||
|
||||
Ajouter les vérifications utiles au ticket ; les essais graphiques du jeu
|
||||
ciblent **Vulkan uniquement**. Le profil ciblé employé pour beta.280 était
|
||||
`-PsanctuaryFocusedTests=recipes,progression -PsanctuaryAtlasOnly=true` : ce reçu
|
||||
historique ne remplace pas les tests d'un futur changement touchant un autre
|
||||
comportement. Consigner les limites, ne pas présenter un contrôle exclu comme
|
||||
réussi.
|
||||
|
||||
Les sorties attendues pour la version choisie sont :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.xxx.jar` : un JAR Sanctuary contenant JEI ;
|
||||
- `build/Sanctuary-beta.xxx.mrpack` : format conservé pour les liens existants ;
|
||||
- `build/Sanctuary-Prism-beta.xxx.zip` : instance prête pour le bouton du site ;
|
||||
- `build/Sanctuary-Server-beta.xxx.zip` : kit Linux ;
|
||||
- les métadonnées client/serveur de `build/packwiz-release/` après publication.
|
||||
|
||||
Vérifier les versions, SHA-256, index et contenus avant publication. Ne pas
|
||||
versionner les binaires, dépendances, mondes ni secrets. Le ZIP Prism contient
|
||||
seulement sa configuration, son icône, le bootstrap et, depuis beta.291,
|
||||
l'installateur habillé avec le logo Sanctuary ; le premier lancement télécharge
|
||||
les mods. [Contrat et maintien de l'outil](packwiz-logo-beta291.md). L'option
|
||||
`--bootstrap-no-update` conserve cet installateur vérifié ; elle ne désactive
|
||||
pas les mises à jour du pack. Ne pas ajouter de réglages ou sauvegardes personnels.
|
||||
|
||||
Depuis beta.295, le JAR client initialise une seule fois le favori Sanctuary,
|
||||
pour les nouvelles installations comme pour les instances déjà reliées au
|
||||
canal. `servers.dat` reste personnel et hors index. Le profil serveur distribue
|
||||
en revanche `server-icon.png` (logo 64 × 64) ; vérifier son hash et l'icône
|
||||
annoncée par le ping après redémarrage. [Contrat](default-server-beta295.md).
|
||||
|
||||
## Publier les artefacts et le canal
|
||||
|
||||
Committer et pousser la branche source vérifiée, puis exécuter depuis cette
|
||||
même branche propre, en remplaçant `beta.xxx` et le chemin des notes :
|
||||
|
||||
```sh
|
||||
python3.14 scripts/publish_pack.py \
|
||||
--notes-file docs/notes-du-ticket.md \
|
||||
--asset build/Sanctuary-beta.xxx.mrpack \
|
||||
--asset build/Sanctuary-Prism-beta.xxx.zip \
|
||||
--asset build/Sanctuary-Server-beta.xxx.zip
|
||||
```
|
||||
|
||||
Cette commande crée/vérifie la release, joint les artefacts, publie ensemble
|
||||
les profils client et serveur et vérifie leurs téléchargements publics.
|
||||
Elle utilise les identifiants du gestionnaire Git en mémoire. Le tag est
|
||||
exactement `beta.xxx`, sans `v`. Une release déjà publiée reste immuable : ne
|
||||
pas déplacer le tag ou remplacer une pièce jointe différente. Une reprise se
|
||||
fait depuis le commit source exact de la release, pas depuis un commit
|
||||
documentaire ultérieur portant le même compteur.
|
||||
|
||||
`publish_pack.py` avance déjà le canal : ne pas faire un deuxième push manuel
|
||||
du pack. Il ne met à jour ni le serveur, ni Prism, ni le téléchargement Web.
|
||||
Tester deux synchronisations avec le véritable installateur dans un dossier
|
||||
isolé ; vérifier l'absence de doublon Sanctuary et la stabilité du second
|
||||
passage. Conserver le reçu et les hashes dans `build/`, hors de Git.
|
||||
|
||||
## Mettre à jour le serveur Beta dédié
|
||||
|
||||
Depuis la préparation beta.292, le profil serveur comprend aussi Lithium et
|
||||
Simple Voice Chat ; Bobby et Entity Culling restent clients. Le vérificateur
|
||||
Web compare les hashes de ces dépendances partagées avec ceux de la CT.
|
||||
Avant la mise en service vocale demandée, vérifier séparément le port UDP
|
||||
annoncé et un échange audible à deux joueurs ; consulter
|
||||
[le contrat beta.292](optional-mods-beta292.md). Ajouter le JAR vocal au pack
|
||||
ne configure pas les redirections ou le pare-feu du serveur.
|
||||
|
||||
La cible est la **CT 350 sur le nœud `gpu`**, dossier
|
||||
`/srv/minecraft/sanctuary-beta`, service `sanctuary-beta.service`, utilisateur
|
||||
`minecraft`. La CT 300 Alpha et les autres serveurs sont hors cible.
|
||||
L'accès et les chemins propres au poste sont dans la compétence locale
|
||||
`sanctuary-updates`, référence `references/local-environment.md`.
|
||||
|
||||
Quand la livraison comprend le serveur, après les builds et avant d'avancer
|
||||
le canal, arrêter proprement la Beta. Attendre la fin de Java et de la
|
||||
sauvegarde, puis sauvegarder son dossier entier et son environnement privé
|
||||
dans un emplacement root privé hors du dossier géré par packwiz. Noter le
|
||||
commit du canal et les hashes précédents. Cela évite que des clients à jour
|
||||
rejoignent brièvement l'ancien serveur.
|
||||
|
||||
Après publication du canal, ces commandes s'exécutent **dans la CT 350** :
|
||||
|
||||
```sh
|
||||
systemctl stop sanctuary-beta.service
|
||||
runuser -u minecraft -- /srv/minecraft/sanctuary-beta/server.sh update
|
||||
systemctl start sanctuary-beta.service
|
||||
systemctl is-active sanctuary-beta.service
|
||||
journalctl -u sanctuary-beta.service -n 100 --no-pager
|
||||
```
|
||||
|
||||
La sauvegarde précède `update` ; le premier `stop` ci-dessus reste nécessaire
|
||||
si cette étape est exécutée séparément. Ne redémarrer qu'après une mise à jour
|
||||
réussie. L'outil refuse un changement implicite de Minecraft/Fabric et une
|
||||
mise à jour concurrente au lancement. Il ne charge pas automatiquement
|
||||
`/etc/sanctuary/beta.env` : reprendre seulement les variables non secrètes
|
||||
nécessaires si le Java ou le canal diffèrent de ceux du kit. Le service charge
|
||||
l'environnement privé au démarrage.
|
||||
|
||||
Conserver le monde, `packwiz.json`, `config/`, `server.properties`,
|
||||
`eula.txt`, les listes de joueurs, opérateurs, permissions et reçus d'admission.
|
||||
Garder l'UUID communautaire existant. Ne pas recréer une CT, réextraire un kit
|
||||
sur tout le serveur ou réinitialiser Gazette/Tableau/Intendance pour une mise
|
||||
à jour courante. [Contrat complet du serveur](server-pack.md).
|
||||
|
||||
Contrôler le JAR effectivement chargé, Fabric API, l'admission sans consommer
|
||||
un vrai code, les logs, le ping Minecraft et la fraîcheur des métriques Web.
|
||||
Un ping depuis le LAN via l'IP publique ne prouve pas un accès depuis un réseau
|
||||
extérieur. Ne pas remettre l'ancien secours UPnP : la règle Bbox permanente
|
||||
existe déjà.
|
||||
|
||||
## Actualiser le téléchargement et la version du site
|
||||
|
||||
Après vérification de la CT Beta, transférer le script versionné
|
||||
`scripts/publish_web_pack.py` sur **`gpu`** dans un dossier temporaire privé et
|
||||
vérifier que sa copie a le même SHA-256. Lancer sur `gpu`, où `pct` existe :
|
||||
|
||||
```sh
|
||||
python3 /chemin/temporaire/publish_web_pack.py \
|
||||
--web-ct 320 --server-ct 350 --version beta.xxx \
|
||||
--mrpack-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.xxx/Sanctuary-beta.xxx.mrpack \
|
||||
--prism-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.xxx/Sanctuary-Prism-beta.xxx.zip \
|
||||
--pack-url https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
Si le contrôle passe, reprendre la même commande sans `--dry-run`. Ce script
|
||||
vérifie le serveur en marche, les deux profils, les archives et leurs hashes,
|
||||
puis bascule atomiquement `storage/app/distribution/current` dans la CT Web.
|
||||
Il ne modifie pas le code du site et ne redémarre pas Minecraft.
|
||||
|
||||
Le footer et le survol du bouton lisent cette publication via
|
||||
`App\Services\PackDistribution`. Ne pas remplacer leurs versions en dur ni
|
||||
reconfigurer le bouton à chaque livraison. Le bouton **Télécharger** donne
|
||||
directement le ZIP prêt ; le joueur l'importe une fois dans Prism puis lance
|
||||
pour télécharger le pack. Aucun guide manuel ni téléchargement séparé ne
|
||||
remplace ce parcours. L'ancien `/downloads/sanctuary-beta.mrpack` reste valide.
|
||||
|
||||
Vérifier le ZIP reçu anonymement (HTTP 200, nom `Sanctuary-Beta.zip`, SHA-256),
|
||||
le footer et le survol, puis le lien MRpack historique. Une bascule de version
|
||||
ne nécessite pas de reconstruire le cache Laravel.
|
||||
|
||||
## Livrer une modification du site
|
||||
|
||||
Travailler dans les sources **Sanctuary Web** et lire leur `AGENTS.md` ainsi
|
||||
que les règles/compétences applicables. Tester les comportements modifiés,
|
||||
formater le PHP avec `vendor/bin/pint --dirty --format agent`, construire les
|
||||
assets concernés et vérifier le rendu navigateur. Les contrôles existants
|
||||
pour cette chaîne sont `tests/Feature/DownloadTest.php` et
|
||||
`tests/Feature/ServerMetricsTest.php`.
|
||||
|
||||
**La CT 320 est un checkout de production avec des changements préexistants.**
|
||||
Ne pas y faire `git pull`, `reset --hard`, `clean`, ou une copie avec suppression
|
||||
depuis `origin/main`. Inspecter les fichiers concernés et leurs hashes, préparer
|
||||
un patch limité conservant les autres différences, sauvegarder les fichiers
|
||||
et caches affectés dans `/root/sanctuary-beta-maintenance/`, puis appliquer
|
||||
avec refus si les fichiers ont changé depuis l'inspection. Conserver
|
||||
propriétaires/modes et écrire les assets avant les vues qui les référencent.
|
||||
|
||||
Ne vider/reconstruire que les caches concernés : `view:clear` pour Blade ;
|
||||
`config:cache` si la configuration change ; `route:cache` si des routes changent
|
||||
et étaient déjà mises en cache. Exécuter Artisan comme `www-data` depuis
|
||||
`/var/www/sanctuary`. Aucun changement de base, réinitialisation de données ou
|
||||
migration ne découle d'une simple livraison de présentation.
|
||||
|
||||
Contrôler le site public, les métriques et la distribution. Les comptes admins,
|
||||
groupes staff et permissions restent identiques ; comparer leurs empreintes si
|
||||
l'intervention touche leur contexte. Les scripts ponctuels dans `build/`
|
||||
restent des reçus historiques, à ne pas rejouer avec leurs anciennes hypothèses.
|
||||
La procédure complémentaire du dépôt Web est `docs/updates.md`.
|
||||
|
||||
## Synchroniser l'instance locale existante
|
||||
|
||||
Retrouver l'instance **Sanctuary Beta** et lire sa configuration. Son dossier
|
||||
historique peut encore s'appeler `Sanctuary-0.1.0-alpha.1` ; ce nom n'indique
|
||||
pas sa version courante. Ne pas créer une nouvelle instance à chaque livraison,
|
||||
ne pas réimporter le ZIP et ne pas toucher aux anciennes instances 26.2.
|
||||
|
||||
Si la demande inclut la mise à jour locale : attendre la fermeture du jeu,
|
||||
sauvegarder les fichiers gérés et les métadonnées de l'instance hors de `mods/`,
|
||||
relever les hashes des données personnelles, puis exécuter sa commande packwiz
|
||||
avant lancement avec ses vrais `INST_JAVA`, `INST_MC_DIR`, `INST_DIR`.
|
||||
Pour une synchronisation sans lancer Minecraft, ajouter `-g` à l'installateur.
|
||||
Lors de la première livraison de l'habillage beta.291, sauvegarder aussi la
|
||||
commande existante et le JAR de l'installateur. Copier l'installateur vérifié du
|
||||
ZIP dans cette même instance et ajouter `--bootstrap-no-update` à sa commande
|
||||
avant lancement ; le canal de mods seul ne remplace pas cet outil. Les prochains
|
||||
changements de l'installateur suivent la même mise à jour explicite et vérifiée.
|
||||
Ne jamais lancer `instance.cfg` comme du shell ni évaluer une chaîne de commande
|
||||
non vérifiée. Les chemins avec espaces doivent rester des arguments séparés.
|
||||
|
||||
Faire deux passages et comparer : un seul JAR Sanctuary attendu, mêmes fichiers
|
||||
gérés au second passage, mêmes données personnelles avant/après. Conserver
|
||||
`saves/`, captures, favoris, réglages, packs graphiques et configurations tierces.
|
||||
Ne pas ouvrir un monde personnel pour prouver une simple synchronisation.
|
||||
|
||||
## Reprise et reçu de livraison
|
||||
|
||||
Pour un échec, conserver l'état/les logs, revenir au dernier artefact immuable
|
||||
et à la sauvegarde compatible. Un retour du canal doit former un nouveau commit
|
||||
de métadonnées sans réécrire l'historique. Une reprise du téléchargement Web
|
||||
rebascule son lien `current` vers une publication complète conservée ; elle ne
|
||||
change pas à elle seule le serveur ou le canal. Ne pas démarrer un ancien JAR
|
||||
sur des données migrées sans vérifier le contrat de sauvegarde.
|
||||
|
||||
Consigner pour chaque livraison : commit source, version/tag, commit du canal,
|
||||
hashes et URL des artefacts, CT/service et version chargée, publication Web,
|
||||
version locale effectivement synchronisée, sauvegardes, vérifications et limites.
|
||||
Mettre à jour la référence d'environnement de la compétence lorsque les cibles
|
||||
ou chemins changent. La mémorisation de cette procédure ne prétend pas que
|
||||
toutes les installations sont déjà synchronisées.
|
||||
+18
-1
@@ -522,7 +522,24 @@ Un sneak + clic droit permet de porter un mob sur sa tête, avec une limite d'un
|
||||
|
||||
### Hostiles et fantômes
|
||||
|
||||
Une famille de zombies inspirée de Left 4 Dead comprend le **Hunter**, le **Charger**, le **Spitter**, le **Boomer** et l'**Infecté**. Le **Tank** et le **zombie géant** sont des boss futurs.
|
||||
Une famille de zombies inspirée de Left 4 Dead comprend le **Hunter**, le **Charger**, le **Spitter**, le **Boomer**, le **Smoker** et l'**Infecté**. Le **Tank** et le **zombie géant** sont des boss futurs.
|
||||
|
||||
Les infectés n'ont pas de variante bébé. Leurs familiers utilisent le modèle
|
||||
adulte en miniature, avec une taille variable propre à chaque individu.
|
||||
Le Spitter dépose une zone d’acide persistante au sol, comme une potion
|
||||
persistante : sortir de la zone arrête les dégâts, sans poison résiduel.
|
||||
Le Boomer projette une bile à courte portée qui attire les zombies présents
|
||||
vers la cible enduite, et éclabousse en mourant sans abîmer le terrain. Son
|
||||
familier affaiblit les ennemis par sa bile et reste vivant après son pouvoir.
|
||||
Le Smoker doit toucher sa cible avec un lasso physique qui peut rater, puis
|
||||
la ramène vers lui par une traction de tyrolienne. Les obstacles ou les
|
||||
blessures de la source coupent ce câble temporaire.
|
||||
Le Hunter prépare puis bondit physiquement vers sa cible. Seul le contact
|
||||
inflige des dégâts ; la direction reste fixe après le départ, permettant
|
||||
l’esquive. Son familier miniature possède le même bond offensif.
|
||||
Depuis beta.287, le zombie classique et ces six infectés partagent neuf tenues
|
||||
aléatoires stables par individu. Tous gardent les bras classiques en avant. Le Hunter
|
||||
conserve sa posture accroupie au sol, familiers compris.
|
||||
|
||||
Le système d'insomnie et les phantoms vanilla disparaissent lorsque le monde passe au temps réel. Ils laissent place à d'autres fantômes, dont une **baleine volante, fantomatique et apaisante**, qui passe autour des joueurs et peut s'inviter dans le paysage comme un photobomb.
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# ZOMBIES-287 — tenues aléatoires et posture du Hunter
|
||||
|
||||
Branche `codex/zombie-appearances`, sur le socle local beta.286 conservé.
|
||||
État : implémenté et vérifié ; pack local beta.287 construit et contrôlé.
|
||||
|
||||
## Contrat
|
||||
|
||||
Les neuf PNG fournis par le créateur remplacent les tenues fixes du zombie
|
||||
classique et des six infectés Sanctuary. Même collection aléatoire pour tous,
|
||||
choix stable par UUID natif ; les familiers utilisent leur identité persistante.
|
||||
Les fichiers adultes sont copiés à l’identique, sans génération d’image.
|
||||
Les bébés du zombie classique gardent leur modèle natif avec une adaptation
|
||||
UV déterministe ; les infectés restent exclusivement adultes, leurs familiers
|
||||
restent des adultes miniatures à taille variable.
|
||||
|
||||
Tous gardent les deux bras en avant avec l’animation native du zombie. Le
|
||||
Hunter conserve sa posture accroupie au sol, avec la marche native des jambes ;
|
||||
il se redresse pendant son bond. La même présentation s’applique aux familiers.
|
||||
La posture ne change ni les collisions, ni la vitesse, ni les dégâts, ni les
|
||||
règles de sauvegarde. Aucun nouvel état réseau ou format persistant n’est ajouté.
|
||||
Aucun changement de génération ou d’identifiant.
|
||||
|
||||
## Vérification
|
||||
|
||||
- `python3 scripts/prepare_zombie_skins.py --check` : neuf fichiers adultes
|
||||
identiques aux PNG fournis et neuf adaptations bébé conformes aux UV natifs
|
||||
de `BabyZombieModel` 26.3. Reçu : `docs/zombie-skins-beta287.json`.
|
||||
- Parcours `:sanctuary:runClientGameTest -PsanctuaryClientTests=true
|
||||
-PsanctuaryZombie287ClientTests=true -PsanctuaryQuickTests=true
|
||||
-PsanctuaryClientGraphicsBackend=vulkan -PsanctuaryClientHeapGb=8
|
||||
-PsanctuaryClientNoVsync=true` : **réussi**. Vulkan / MoltenVK 1.4.2 /
|
||||
Apple M1, monde plat de développement temporaire, graine `287`.
|
||||
Neuf tenues accessibles pour chacun des sept types ; texture stable après
|
||||
mouvement et reconstruction avec la même identité ; bébé classique avec son
|
||||
modèle natif ; les deux bras natifs en avant pour tous ; 21 combinaisons de
|
||||
familiers (sept types × tailles 500/1000/1500), proportions adultes des
|
||||
infectés, rééquipement sans changement de tenue ; Hunter réellement en marche
|
||||
accroupie, puis redressé et en l’air pendant son bond.
|
||||
Captures finales : `build/zombie287-screenshots/`.
|
||||
- Régressions `ziplines242,boomer284,spitter283,charger282,infected280,familiar260,companions` :
|
||||
**77 tests requis réussis**, en session distincte des joueurs simulés du Smoker
|
||||
et des arènes du Hunter.
|
||||
- Régression `smoker285` dans une autre session : **19 tests requis réussis**.
|
||||
Les comptes incluent chacun leur test d’infrastructure.
|
||||
- `./gradlew check build assemblePack -PsanctuaryFocusedTests=hunter286
|
||||
-PsanctuaryAtlasOnly=true` : **réussi en 2 min 40 s**, y compris les
|
||||
**16 tests Hunter** et tous les contrôles purs configurés par `check`.
|
||||
- Export packwiz, intégrité ZIP et métadonnées vérifiés : `beta.287`,
|
||||
Minecraft `26.3`, Fabric Loader `0.19.5`, un seul JAR Sanctuary identique
|
||||
au fichier construit. Les dix-huit PNG correspondent au reçu ; les neuf
|
||||
adultes correspondent aussi aux fichiers fournis. Classes de test exclues,
|
||||
rendu natif des bras confirmé, archive beta.286 conservée à l’identique.
|
||||
|
||||
## Artefacts locaux
|
||||
|
||||
- [Pack beta.287](../build/Sanctuary-beta.287.mrpack), 16 713 076 octets.
|
||||
SHA-256 : `58c9375b8de43f77d55114bc8683a524416a4cb13eca7c6e18cb6fd93c3fd6f0`.
|
||||
- JAR `mods/sanctuary/build/libs/sanctuary-beta.287.jar`.
|
||||
SHA-256 : `26ba7a1f488896dd7e099e24113a1049ec96ea95f3dbcf0559456bd9cb688324`.
|
||||
- Reçu détaillé : `build/zombie287-validation.json`. Logs et captures finales
|
||||
ignorés sous `build/zombie287-*` ; aperçu recadré de la collection :
|
||||
`build/zombie287-wardrobe-preview.png`.
|
||||
|
||||
## Limites
|
||||
|
||||
La collection concerne le zombie classique et les six infectés ajoutés ici.
|
||||
Les noyés, husks et zombies villageois gardent leurs modèles et tenues propres.
|
||||
Les neuf fichiers adultes sont exploités tels quels ; seuls les dérivés destinés
|
||||
au bébé classique sont rééchantillonnés. Aucun infecté bébé n’est introduit.
|
||||
Les sauvegardes et réglages d’une installation personnelle ne sont pas touchés.
|
||||
@@ -0,0 +1,65 @@
|
||||
[
|
||||
{
|
||||
"source_file": "zombie.png",
|
||||
"asset": "zombie1.png",
|
||||
"sha256": "37cb6e101f6832172bd597341a6b1e37b215268e7ece5256079b4df30a98b4b4",
|
||||
"baby_asset": "zombie1_baby.png",
|
||||
"baby_sha256": "46e85f677761df2c54bed402fcc7f080342ad004e68375ad60494c229828be4d"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie2.png",
|
||||
"asset": "zombie2.png",
|
||||
"sha256": "3c84f78cd6c3beac5cd27b9ac4d2da4b8d0a679326dcdfc991271359aababbe6",
|
||||
"baby_asset": "zombie2_baby.png",
|
||||
"baby_sha256": "dbaf18b2c026b01c26dc9b3f156ca8757d43acca64a85a335e980d53046c6a58"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie3.png",
|
||||
"asset": "zombie3.png",
|
||||
"sha256": "af3e6a9f529276b853609e8389a6dea1aa92d3f2cdae6fdecb906424b959794a",
|
||||
"baby_asset": "zombie3_baby.png",
|
||||
"baby_sha256": "18249a78f397937ffdb335cf9eb3558c0a24a7bf4f488056c37227b94f880ba6"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie4.png",
|
||||
"asset": "zombie4.png",
|
||||
"sha256": "f7b4b9024efc810aef3c768a7a7024dbf90d2fabffd06031551d3f4bad8f4cd7",
|
||||
"baby_asset": "zombie4_baby.png",
|
||||
"baby_sha256": "55df3c6b44096503ca38ee5ad5322bfc957f993ebd1269b59038c8a95d7035e4"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie5.png",
|
||||
"asset": "zombie5.png",
|
||||
"sha256": "dc6b84f3f9b679c2de8c7cf29eaa0feb6b2762f0c9854d86a26f45123e697feb",
|
||||
"baby_asset": "zombie5_baby.png",
|
||||
"baby_sha256": "8fd1e604493ff472657d9162abb4b1144674cb4c8a4a333d32d7335bff9da018"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie6.png",
|
||||
"asset": "zombie6.png",
|
||||
"sha256": "948f1e9823a15ace01bbc3fdeae8f3a8448e4fc4f90c2d79f35fe95dac3f9e37",
|
||||
"baby_asset": "zombie6_baby.png",
|
||||
"baby_sha256": "08a008c5b0618242784381a74955e20ddcfcc4752b739b07eba318e1c9553daa"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie7.png",
|
||||
"asset": "zombie7.png",
|
||||
"sha256": "3494c1ecff8bd5dd39d483f8de3747c784dbef161a96aa731e505eea25fdd4bd",
|
||||
"baby_asset": "zombie7_baby.png",
|
||||
"baby_sha256": "661c6ac403226c38d15d7af40318ef4d1ccf71e9e7d4d34f69b5b182cb4e45bf"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie8.png",
|
||||
"asset": "zombie8.png",
|
||||
"sha256": "d9798a366c0c91629ceb7bb4414b8deaf85516e733ee6d1ff11efa568125356f",
|
||||
"baby_asset": "zombie8_baby.png",
|
||||
"baby_sha256": "5ea53fbe31de2ec5286906fc4589a6adf547d0b3c0c991dd81c7226a00dbcffb"
|
||||
},
|
||||
{
|
||||
"source_file": "zombie9.png",
|
||||
"asset": "zombie9.png",
|
||||
"sha256": "99e918bebd7f449e9c36c123b0b4b4ad86cc0091d276785674180bd07361886b",
|
||||
"baby_asset": "zombie9_baby.png",
|
||||
"baby_sha256": "3391e90916558dfe31c08a9e298f058f917b0d4c64027f9deff5355912c9458d"
|
||||
}
|
||||
]
|
||||
+2
-2
@@ -9,8 +9,8 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.5+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.280
|
||||
pack_version=beta.280
|
||||
mod_version=beta.295
|
||||
pack_version=beta.295
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.4
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Adapted from packwiz-installer v0.5.14 InstallWindow.kt.
|
||||
* Copyright (c) 2021 comp500. MIT license, see LICENSE.
|
||||
* Sanctuary branding: KOKA99CAB, 2026.
|
||||
*/
|
||||
package link.infra.packwiz.installer.ui.gui;
|
||||
|
||||
import link.infra.packwiz.installer.ui.data.InstallProgress;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Image;
|
||||
import java.util.Locale;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
|
||||
/** Presentation only: GUIHandler retains all packwiz installation decisions. */
|
||||
public final class InstallWindow extends JFrame {
|
||||
private final boolean french = "fr".equals(Locale.getDefault().getLanguage());
|
||||
private final JLabel progressLabel;
|
||||
private final JProgressBar progressBar;
|
||||
private final JButton options;
|
||||
private final JButton cancel;
|
||||
private final JButton ok;
|
||||
private final JPanel buttons;
|
||||
|
||||
public InstallWindow(GUIHandler handler) {
|
||||
setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||
setResizable(false);
|
||||
|
||||
ImageIcon logo = new ImageIcon(InstallWindow.class.getResource("/sanctuary/packwiz/logo.png"));
|
||||
JPanel header = new JPanel(new BorderLayout());
|
||||
header.setBackground(Color.BLACK);
|
||||
header.setBorder(new EmptyBorder(14, 24, 4, 24));
|
||||
JLabel wordmark = new JLabel(new ImageIcon(
|
||||
logo.getImage().getScaledInstance(640, 160, Image.SCALE_AREA_AVERAGING)));
|
||||
wordmark.getAccessibleContext().setAccessibleName("Sanctuary");
|
||||
header.add(wordmark, BorderLayout.CENTER);
|
||||
add(header, BorderLayout.NORTH);
|
||||
|
||||
JPanel progress = new JPanel(new BorderLayout(0, 10));
|
||||
progress.setBorder(new EmptyBorder(20, 24, 12, 24));
|
||||
progressBar = new JProgressBar();
|
||||
progressBar.setIndeterminate(true);
|
||||
progressBar.getAccessibleContext().setAccessibleName(text("Progression", "Progress"));
|
||||
progressLabel = new JLabel(text("Chargement du pack…", "Loading the pack…"));
|
||||
// Keep long filenames from widening the window beyond the screen.
|
||||
progressLabel.setPreferredSize(new Dimension(640, 24));
|
||||
progress.add(progressBar, BorderLayout.NORTH);
|
||||
progress.add(progressLabel, BorderLayout.SOUTH);
|
||||
add(progress, BorderLayout.CENTER);
|
||||
|
||||
buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
|
||||
buttons.setBorder(new EmptyBorder(0, 16, 18, 16));
|
||||
options = new JButton(text("Mods optionnels…", "Optional mods…"));
|
||||
options.addActionListener(event -> {
|
||||
options.setText(text("Chargement…", "Loading…"));
|
||||
options.setEnabled(false);
|
||||
handler.setOptionsButtonPressed(true);
|
||||
});
|
||||
cancel = new JButton(text("Annuler", "Cancel"));
|
||||
cancel.addActionListener(event -> {
|
||||
cancel.setEnabled(false);
|
||||
handler.setCancelButtonPressed(true);
|
||||
});
|
||||
ok = new JButton(text("Continuer", "Continue"));
|
||||
ok.addActionListener(event -> handler.setOkButtonPressed(true));
|
||||
buttons.add(options);
|
||||
buttons.add(cancel);
|
||||
add(buttons, BorderLayout.SOUTH);
|
||||
pack();
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
|
||||
private String text(String fr, String en) {
|
||||
return french ? fr : en;
|
||||
}
|
||||
|
||||
public final void displayProgress(InstallProgress progress) {
|
||||
progressBar.setIndeterminate(!progress.getHasProgress());
|
||||
progressBar.setStringPainted(progress.getHasProgress());
|
||||
if (progress.getHasProgress()) {
|
||||
progressBar.setMaximum(progress.getProgressTotal());
|
||||
progressBar.setValue(progress.getProgress());
|
||||
progressBar.setString(progress.getProgress() + " / " + progress.getProgressTotal());
|
||||
} else {
|
||||
progressBar.setValue(0);
|
||||
}
|
||||
progressLabel.setText(progress.getMessage());
|
||||
progressLabel.setToolTipText(progress.getMessage());
|
||||
}
|
||||
|
||||
public final void disableOptionsButton(boolean hasOptions) {
|
||||
options.setText(hasOptions ? text("Mods optionnels…", "Optional mods…")
|
||||
: text("Aucun mod optionnel", "No optional mods"));
|
||||
options.setEnabled(false);
|
||||
}
|
||||
|
||||
public final void showOk(boolean hideCancel) {
|
||||
if (hideCancel) {
|
||||
buttons.remove(cancel);
|
||||
}
|
||||
if (ok.getParent() != buttons) {
|
||||
buttons.add(ok);
|
||||
}
|
||||
buttons.revalidate();
|
||||
buttons.repaint();
|
||||
}
|
||||
|
||||
public final void hideOk() {
|
||||
buttons.remove(ok);
|
||||
if (cancel.getParent() != buttons) {
|
||||
buttons.add(cancel);
|
||||
}
|
||||
buttons.revalidate();
|
||||
buttons.repaint();
|
||||
}
|
||||
|
||||
public final void timeoutOk(long remaining) {
|
||||
// Upstream GUIHandler invokes this callback from its countdown timer.
|
||||
if (!SwingUtilities.isEventDispatchThread()) {
|
||||
SwingUtilities.invokeLater(() -> timeoutOk(remaining));
|
||||
return;
|
||||
}
|
||||
ok.setText(text("Continuer", "Continue") + " (" + remaining + ")");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* Swing integration check for the adapted window and the upstream GUIHandler. */
|
||||
import java.awt.Component;
|
||||
import java.awt.Container;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Window;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.Locale;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.SwingUtilities;
|
||||
import link.infra.packwiz.installer.ui.data.InstallProgress;
|
||||
import link.infra.packwiz.installer.ui.gui.GUIHandler;
|
||||
import link.infra.packwiz.installer.ui.gui.InstallWindow;
|
||||
|
||||
public final class InstallWindowCheck {
|
||||
private static <T extends Component> T find(Container root, Class<T> type, String text) {
|
||||
for (Component child : root.getComponents()) {
|
||||
if (type.isInstance(child) && (text == null ||
|
||||
(child instanceof JButton && ((JButton) child).getText().equals(text)))) {
|
||||
return type.cast(child);
|
||||
}
|
||||
if (child instanceof Container) {
|
||||
T nested = find((Container) child, type, text);
|
||||
if (nested != null) return nested;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void require(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
boolean fr = "fr".equals(Locale.getDefault().getLanguage());
|
||||
GUIHandler handler = new GUIHandler();
|
||||
try {
|
||||
handler.setTitle("Sanctuary · packwiz");
|
||||
handler.show();
|
||||
SwingUtilities.invokeAndWait(() -> {});
|
||||
InstallWindow window = null;
|
||||
for (Window candidate : Window.getWindows()) {
|
||||
if (candidate instanceof InstallWindow) window = (InstallWindow) candidate;
|
||||
}
|
||||
final InstallWindow frame = window;
|
||||
require(frame != null && frame.isVisible(), "Upstream handler did not show branded window");
|
||||
handler.submitProgress(new InstallProgress(fr ? "Téléchargement des mods…" : "Downloading mods…", 3, 10));
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
JProgressBar bar = find(frame, JProgressBar.class, null);
|
||||
require(bar != null && !bar.isIndeterminate() && bar.getValue() == 3
|
||||
&& bar.getMaximum() == 10, "Upstream progress was not preserved");
|
||||
frame.showOk(false);
|
||||
});
|
||||
// Exercise the timer callback from outside the Swing thread.
|
||||
frame.timeoutOk(5);
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
JButton next = find(frame, JButton.class, fr ? "Continuer (5)" : "Continue (5)");
|
||||
require(next != null, "Countdown or locale broken");
|
||||
next.doClick();
|
||||
require(handler.getOkButtonPressed(), "Continue no longer reaches upstream handler");
|
||||
frame.hideOk();
|
||||
// Capture actual Swing layout, including the bundled logo, in both locales.
|
||||
BufferedImage image = new BufferedImage(frame.getWidth(), frame.getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
frame.paint(graphics);
|
||||
graphics.dispose();
|
||||
try { ImageIO.write(image, "png", new File(args[0])); }
|
||||
catch (Exception error) { throw new RuntimeException(error); }
|
||||
frame.showOk(true);
|
||||
require(find(frame, JButton.class, fr ? "Annuler" : "Cancel") == null,
|
||||
"Cancel should be hidden during the optional-mod countdown");
|
||||
frame.hideOk();
|
||||
JButton options = find(frame, JButton.class, fr ? "Mods optionnels…" : "Optional mods…");
|
||||
options.doClick();
|
||||
require(handler.getOptionsButtonPressed() && !options.isEnabled(), "Options callback broken");
|
||||
frame.disableOptionsButton(false);
|
||||
require(find(frame, JButton.class, fr ? "Aucun mod optionnel" : "No optional mods") != null,
|
||||
"Empty options label broken");
|
||||
find(frame, JButton.class, fr ? "Annuler" : "Cancel").doClick();
|
||||
require(handler.getCancelButtonPressed(), "Cancellation callback broken");
|
||||
frame.displayProgress(new InstallProgress("Loading…"));
|
||||
require(find(frame, JProgressBar.class, null).isIndeterminate(), "Indeterminate progress broken");
|
||||
});
|
||||
System.out.println("PASS: " + Locale.getDefault() + " logo/progress/options/continue/countdown/cancel");
|
||||
} finally {
|
||||
handler.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 comp500
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Fenêtre packwiz Sanctuary
|
||||
|
||||
`sanctuary.png` est le bandeau fourni par le créateur, conservé sans modification.
|
||||
`InstallWindow.java` adapte la seule fenêtre de progression de packwiz-installer
|
||||
v0.5.14, sous MIT. Le moteur officiel et ses autres interfaces restent intacts.
|
||||
Le contrat de distribution est dans [le ticket beta.291](../../docs/packwiz-logo-beta291.md).
|
||||
|
||||
Construire avec le JDK 25 utilisé par le dépôt :
|
||||
|
||||
```sh
|
||||
python3.14 scripts/packwiz_installer.py
|
||||
python3.14 scripts/test_packwiz_installer.py
|
||||
```
|
||||
|
||||
Le premier appel télécharge l'amont vérifié dans `build/packwiz-branding/`.
|
||||
Le JAR Sanctuary généré doit correspondre à `installer.json` ; ce verrou et
|
||||
`INSTALLER_SHA256` de `scripts/publish_web_pack.py` évoluent ensemble après revue
|
||||
et vérification de la nouvelle fenêtre. Ne pas remplacer le moteur ni désactiver
|
||||
le contrôle de hash pour faire passer une compilation différente.
|
||||
|
||||
Le parcours Swing réel FR/EN se vérifie sur un poste graphique, sans ouvrir
|
||||
Minecraft :
|
||||
|
||||
```sh
|
||||
javac -encoding UTF-8 -classpath build/packwiz-branding/packwiz-installer-sanctuary.jar \
|
||||
-d build/packwiz-branding/check launcher/packwiz/InstallWindowCheck.java
|
||||
java -Duser.language=fr -classpath build/packwiz-branding/check:build/packwiz-branding/packwiz-installer-sanctuary.jar \
|
||||
InstallWindowCheck build/packwiz-branding/preview-fr.png
|
||||
java -Duser.language=en -classpath build/packwiz-branding/check:build/packwiz-branding/packwiz-installer-sanctuary.jar \
|
||||
InstallWindowCheck build/packwiz-branding/preview-en.png
|
||||
```
|
||||
|
||||
Le séparateur de classpath est `;` sous Windows. Le parcours contrôle les
|
||||
callbacks du vrai `GUIHandler`, la progression et le compte à rebours ; il
|
||||
capture la fenêtre et la ferme. Les captures ne font pas partie du JAR livré.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"upstream_version": "v0.5.14",
|
||||
"upstream_url": "https://github.com/packwiz/packwiz-installer/releases/download/v0.5.14/packwiz-installer.jar",
|
||||
"upstream_sha256": "c9f646908d340d84773948a9a7d98bc1dae250d35e1016dc6e2b8459760b5598",
|
||||
"logo_sha256": "efee73a6c812c89b2900d41f6446c0f906ccf85136b6c226daf42870a41a5157",
|
||||
"installer_sha256": "9e2912fdd05e5c23721acd58cd5c61bbf13061e453627e8f4df9a4c53f7aa875"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -64,6 +64,7 @@ if (clientRenderTests) {
|
||||
systemProperty('sanctuary.test.improvedTransparency', improvedTransparency.get())
|
||||
}
|
||||
systemProperty('sanctuary.test.external257Integration', providers.gradleProperty('sanctuaryExternal257Integration').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.optional292WhaleOnly', providers.gradleProperty('sanctuaryOptional292WhaleOnly').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.ssr', providers.gradleProperty('sanctuarySsrClientTests').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.shaderPerformance254', providers.gradleProperty('sanctuaryShaderPerformance254').getOrElse('false'))
|
||||
systemProperty('sanctuary.test.shaderMovingSun254', providers.gradleProperty('sanctuaryShaderMovingSun254').getOrElse('false'))
|
||||
@@ -151,6 +152,15 @@ tasks.named('runGameTest') {
|
||||
}
|
||||
|
||||
tasks.named('processGametestResources') {
|
||||
def defaultServer295 = providers.gradleProperty('sanctuaryDefaultServer295ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryDefaultServer295ClientTests', defaultServer295)
|
||||
if (defaultServer295) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.DefaultServer295ClientChecks') }
|
||||
def happyCreeper = providers.gradleProperty('sanctuaryHappyCreeperClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryHappyCreeperClientTests', happyCreeper)
|
||||
if (happyCreeper) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.HappyCreeperClientChecks') }
|
||||
def optional292 = providers.gradleProperty('sanctuaryOptional292ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryOptional292ClientTests', optional292)
|
||||
if (optional292) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Optional292ClientChecks') }
|
||||
def external257 = providers.gradleProperty('sanctuaryExternal257ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryExternal257ClientTests', external257)
|
||||
if (external257) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.External257ClientChecks') }
|
||||
@@ -208,6 +218,10 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('sanctuaryColored139ClientTests', colored139Client)
|
||||
if (colored139Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.ColoredLights139ClientChecks') }
|
||||
|
||||
def handLightClient = providers.gradleProperty('sanctuaryHandLightClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryHandLightClientTests', handLightClient)
|
||||
if (handLightClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.HandLight283ClientChecks') }
|
||||
|
||||
def pbr134Client = providers.gradleProperty('sanctuaryPbr134ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPbr134ClientTests', pbr134Client)
|
||||
if (pbr134Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.shader.Pbr134ClientChecks') }
|
||||
@@ -278,7 +292,7 @@ tasks.named('processGametestResources') {
|
||||
|
||||
def villageOnly = providers.gradleProperty('sanctuaryVillageOnly').map { it.toBoolean() }.getOrElse(false)
|
||||
def focused = providers.gradleProperty('sanctuaryFocusedTests').getOrElse('').split(',').findAll { !it.isEmpty() }
|
||||
def focusClasses = [claims278: 'ClaimBattle278GameTests', claims277: 'ClaimDemeure277GameTests', menu275: 'Menu275GameTests', claims273: 'ClaimMap273GameTests', claims272: 'ChunkClaims272GameTests', overview271: 'Inhabitant271GameTests', property270: 'Property270GameTests', providence269: 'Providence269GameTests', providence268: 'Providence268GameTests', providence265: 'Providence265GameTests', familiar260: 'Familiar260GameTests', census: 'CensusGameTestsTerrain', insects245: 'Insect245GameTests', map244: 'Atlas244GameTests', ziplines242: 'Zipline242GameTests', waystones239: 'Waystone239GameTests', waystonesLarge239: 'WaystoneLarge239GameTests', forgerie238: 'Forgerie238GameTests', aptitudes236: 'Aptitudes236GameTests', solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
|
||||
def focusClasses = [happycreeper: 'HappyCreeperGameTests', whale290: 'FlyingWhale290GameTests', hunter286: 'Hunter286GameTests', smoker285: 'Smoker285GameTests', boomer284: 'Boomer284GameTests', spitter283: 'Spitter283GameTests', charger282: 'Charger282GameTests', infected280: 'Infected280GameTests', claims278: 'ClaimBattle278GameTests', claims277: 'ClaimDemeure277GameTests', menu275: 'Menu275GameTests', claims273: 'ClaimMap273GameTests', claims272: 'ChunkClaims272GameTests', overview271: 'Inhabitant271GameTests', property270: 'Property270GameTests', providence269: 'Providence269GameTests', providence268: 'Providence268GameTests', providence265: 'Providence265GameTests', familiar260: 'Familiar260GameTests', census: 'CensusGameTestsTerrain', insects245: 'Insect245GameTests', map244: 'Atlas244GameTests', ziplines242: 'Zipline242GameTests', waystones239: 'Waystone239GameTests', waystonesLarge239: 'WaystoneLarge239GameTests', forgerie238: 'Forgerie238GameTests', aptitudes236: 'Aptitudes236GameTests', solo232: 'Solo232GameTests', diagonal231: 'Diagonal231GameTests', diagonal230: 'Diagonal230GameTests', diagonal229: 'Diagonal229GameTests', diagonal228: 'Diagonal228GameTests', diagonal227: 'Diagonal227GameTests', diagonal226: 'Diagonal226GameTests', diagonal225: 'Diagonal225GameTests', east224: 'East224GameTests', east223: 'East223GameTests', rock222: 'YellowRock222GameTests', west221: 'West221GameTests', west220: 'West220GameTests', west219: 'West219GameTests', west218: 'West218GameTests', west217: 'West217GameTests', south216: 'South216GameTests', relays215: 'Relays215GameTests', south214: 'South214GameTests', south213: 'South213GameTests', south212: 'South212GameTests', south211: 'South211GameTests', south210: 'South210GameTests', south209: 'South209GameTests', north208: 'North208GameTests', north207: 'North207GameTests', north206: 'North206GameTests', north205: 'North205GameTests', base204: 'Base204GameTests', palace173: 'Palace173GameTests', horde172: 'Horde172GameTests', horde171: 'Horde171GameTests', horde170: 'Horde170GameTests', horde169: 'Horde169GameTests', horde168: 'Horde168GameTests', horde167: 'Horde167GameTests', plans072: 'Plans072GameTests', starter052: 'Starter052GameTests', dispenser051: 'Dispenser051GameTests', head050: 'Head050GameTests', boatshead: 'BoatsHead049GameTests', poultry: 'Poultry048GameTests', starinspect: 'StarInspection047GameTests', skyflight: 'SkyFlight046GameTests', eggaudit: 'Companion044GameTests', headblocks: 'HeadBlocks043GameTests', operator: 'OperatorStars040GameTests', starter: 'Starter039GameTests', gravelayout: 'GraveInventory039GameTests', foodknowledge: 'FoodKnowledge039GameTests', graves: 'GravesFood038GameTests', realtime: 'Realtime037GameTests', corrections: 'Corrections036GameTests', collections: 'Collections035GameTests', notifications: 'Notifications034GameTests', menus: 'Menus033GameTests', aerial: 'Aerial24WorldGameTests', transit: 'Transit29WorldGameTests', village: 'Village306GameTests', drained: 'Drained307GameTests', expeditions: 'AncientExpeditions001GameTests', progression: 'Progression003GameTests', map: 'Atlas005GameTests', demeure: 'Demeure006GameTests', mining: 'Mining008GameTests', building: 'Building009GameTests', inventory: 'Inventory010GameTests', cycle: 'Cycle011GameTests', inventoryflow: 'Inventory012GameTests', sorting: 'Inventory014GameTests', tutorial: 'Tutorial016GameTests', movement: 'Movement017GameTests', accessories: 'Accessory018GameTests', companions: 'Companion019GameTests', familiar: 'Familiar020GameTests', carry: 'Carry027GameTests', familiarhit: 'Familiar029GameTests', refonte: 'Companion032GameTests', lights: 'Dynamic030GameTests', cosmetics: 'HeadCosmetic021GameTests', startup: 'StartupPerformanceGameTests', recipes: 'Recipes024GameTests']
|
||||
focused.each { if (!focusClasses.containsKey(it)) throw new GradleException('Unknown focused native test: ' + it) }
|
||||
inputs.property('focusedNative', focused)
|
||||
def yellowRock222Client = providers.gradleProperty('sanctuaryYellowRock222ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
@@ -400,6 +414,33 @@ tasks.named('processGametestResources') {
|
||||
inputs.property('insects245Client', insects245Client)
|
||||
if (insects245Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Insect245ClientChecks') }
|
||||
|
||||
def zombie287Client = providers.gradleProperty('sanctuaryZombie287ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('zombie287Client', zombie287Client)
|
||||
if (zombie287Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Zombie287ClientChecks') }
|
||||
def whale290Client = providers.gradleProperty('sanctuaryWhale290ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('whale290Client', whale290Client)
|
||||
if (whale290Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.FlyingWhale290ClientChecks') }
|
||||
def hunter286Client = providers.gradleProperty('sanctuaryHunter286ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('hunter286Client', hunter286Client)
|
||||
if (hunter286Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Hunter286ClientChecks') }
|
||||
def smoker285Client = providers.gradleProperty('sanctuarySmoker285ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('smoker285Client', smoker285Client)
|
||||
if (smoker285Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Smoker285ClientChecks') }
|
||||
def boomer284Client = providers.gradleProperty('sanctuaryBoomer284ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('boomer284Client', boomer284Client)
|
||||
if (boomer284Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Boomer284ClientChecks') }
|
||||
def spitter283Client = providers.gradleProperty('sanctuarySpitter283ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('spitter283Client', spitter283Client)
|
||||
if (spitter283Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Spitter283ClientChecks') }
|
||||
def charger282Client = providers.gradleProperty('sanctuaryCharger282ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('charger282Client', charger282Client)
|
||||
if (charger282Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Charger282ClientChecks') }
|
||||
def infected280Client = providers.gradleProperty('sanctuaryInfected280ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('infected280Client', infected280Client)
|
||||
if (infected280Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Infected280ClientChecks') }
|
||||
def map279Client = providers.gradleProperty('sanctuaryMap279ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('map279Client', map279Client)
|
||||
if (map279Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Map279ClientChecks') }
|
||||
def map244Client = providers.gradleProperty('sanctuaryMap244ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('map244Client', map244Client)
|
||||
if (map244Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Map244ClientChecks') }
|
||||
@@ -670,6 +711,20 @@ tasks.named('processGametestResources') {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Extra mods are independent pack dependencies, never nested in Sanctuary.
|
||||
def extras = new groovy.json.JsonSlurper().parse(rootProject.file('scripts/data/optional-mods.json'))
|
||||
def clientExtras = providers.gradleProperty('sanctuaryOptional292ClientTests').map { it.toBoolean() }.getOrElse(false) || providers.gradleProperty('sanctuaryOptionalModsRuntime').map { it.toBoolean() }.getOrElse(false)
|
||||
def serverExtras = providers.gradleProperty('sanctuaryOptional292ServerTests').map { it.toBoolean() }.getOrElse(false)
|
||||
if (clientExtras || serverExtras) {
|
||||
extras.each { slug, pin ->
|
||||
if (clientExtras || pin.side == 'both') {
|
||||
runtimeOnly("maven.modrinth:${pin.project}:${pin.artifact}") { transitive = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clientRenderTests) extras.each { slug, pin ->
|
||||
compileOnly("maven.modrinth:${pin.project}:${pin.artifact}") { transitive = false }
|
||||
}
|
||||
// Optional adapter is compiled against the exact pack version, never embedded.
|
||||
compileOnly 'maven.modrinth:AANobbMI:bAZQdGpg'
|
||||
if (providers.gradleProperty('sanctuarySodiumClientTests').map { it.toBoolean() }.getOrElse(false)) {
|
||||
@@ -1553,3 +1608,10 @@ tasks.register('claimDuel278Smoke', JavaExec) {
|
||||
mainClass = 'fr.koka.sanctuary.property.ClaimDuel278Smoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('claimDuel278Smoke') }
|
||||
|
||||
tasks.register('defaultServerListSmoke', JavaExec) {
|
||||
dependsOn('testClasses')
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = 'fr.koka.sanctuary.client.DefaultServerListSmoke'
|
||||
}
|
||||
tasks.named('check') { dependsOn('defaultServerListSmoke') }
|
||||
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.BoomerEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Boomer284ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<BoomerEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var preparing=new AtomicBoolean();var coated=new AtomicBoolean();var lured=new AtomicBoolean();
|
||||
var cow=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.animal.cow.Cow>();
|
||||
var recruit=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{var mob=wild.get();if(mob!=null&&!mob.isRemoved()){
|
||||
preparing.set(mob.windingUp());if(cow.get()!=null)coated.set(cow.get().hasEffect(net.minecraft.world.effect.MobEffects.GLOWING));
|
||||
if(recruit.get()!=null)lured.set(recruit.get().getTarget()==cow.get());
|
||||
}});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Boomer284-"+UUID.randomUUID());s.setSeed("284");s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
for(int x=-16;x<=16;x++)for(int z=-16;z<=16;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-2;x<=2;x++)p.level().setBlockAndUpdate(new BlockPos(x,0,3),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.BOOMER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.BOOMER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:boomer"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.BOOMER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/boomer.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.BOOMER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
var pet=FamiliarService.active(p);pet.teleportTo(.5,1,2.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,4.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);c.waitTicks(24);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(enemy.get().getHealth()<100&&enemy.get().hasEffect(net.minecraft.world.effect.MobEffects.WEAKNESS)&&enemy.get().hasEffect(net.minecraft.world.effect.MobEffects.GLOWING),"Real G splashes, weakens and reveals the enemy");
|
||||
check(pet.isAlive()&&pet.getHealth()==14&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"G keeps the miniature alive and commits cooldown");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("boomer284-bile-burst");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(20);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(FamiliarBattle.state(p).cast==null,"Signature and first automatic preparation finish normally");
|
||||
enemy.get().setPermanentlyInvulnerable(true);enemy.get().removeAllEffects();enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("boomer284-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.BOOMER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.BOOMER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Gerbe de bile":"Bile spray";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("boomer284-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
var target=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);target.setPos(.5,1,8.5);p.level().addFreshEntity(target);cow.set(target);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setPos(-3.5,1,10.5);p.level().addFreshEntity(zombie);recruit.set(zombie);
|
||||
var mob=wild.get();mob.setPermanentlyInvulnerable(false);mob.setTarget(target);mob.setOnGround(true);mob.setNoAi(false);
|
||||
});
|
||||
c.waitFor(m->preparing.get(),200);c.waitTicks(2);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("boomer284-windup");
|
||||
c.waitFor(m->coated.get()&&lured.get(),150);c.waitTicks(3);c.takeScreenshot("boomer284-wild-bile");
|
||||
server.runOnServer(s->{check(cow.get().hasEffect(net.minecraft.world.effect.MobEffects.WEAKNESS)&&recruit.get().getTarget()==cow.get(),"Wild bile marks its victim and redirects an existing native zombie");
|
||||
var mob=wild.get();mob.setNoAi(true);recruit.get().setNoAi(true);recruit.get().teleportTo(-5.5,1,9.5);
|
||||
cow.get().teleportTo(mob.getX(),1,mob.getZ()+1.5);cow.get().setHealth(100);cow.get().setInvulnerableTime(0);cow.get().removeAllEffects();
|
||||
mob.hurtServer((net.minecraft.server.level.ServerLevel)mob.level(),mob.damageSources().generic(),100);
|
||||
check(!mob.isAlive()&&cow.get().getHealth()==98&&cow.get().hasEffect(net.minecraft.world.effect.MobEffects.GLOWING),"Native lethal damage triggers the Boomer's harmless-to-terrain death splash");
|
||||
});c.waitTicks(2);c.takeScreenshot("boomer284-death-splash");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
wild.get().discard();wild.set(null);cow.get().discard();enemy.get().discard();recruit.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(20)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("BOOMER284_CLIENT_PASS: Vulkan, adult skin, three miniature sizes, real G bile/effects/cooldown without sacrifice, wild windup/bile/recruitment/death splash, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.ChargerEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Charger282ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<ChargerEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var phase=new java.util.concurrent.atomic.AtomicReference<>(ChargerEntity.Phase.IDLE);
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{var mob=wild.get();if(mob!=null&&!mob.isRemoved())phase.set(mob.phase());});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Charger282-"+UUID.randomUUID());s.setSeed("282");s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
for(int x=-16;x<=16;x++)for(int z=-16;z<=16;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-2;x<=2;x++)p.level().setBlockAndUpdate(new BlockPos(x,0,3),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.CHARGER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.CHARGER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:charger"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.CHARGER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/charger.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.CHARGER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
var pet=FamiliarService.active(p);pet.teleportTo(.5,1,1.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,6.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);c.waitTicks(90);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(enemy.get().getHealth()<100,"Native G input triggers a real straight familiar charge and impact: ready="+FamiliarBattle.bond(p).readyAt()+" pet="+FamiliarService.active(p).position()+" cast="+FamiliarBattle.state(p).cast+" message="+(FamiliarClient.view==null?null:FamiliarClient.view.message()));
|
||||
check(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&FamiliarBattle.state(p).cast==null,"G commits cooldown and finishes recovery");
|
||||
enemy.get().setPermanentlyInvulnerable(true);enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});c.waitTicks(10);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("charger282-adult-zombie");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("charger282-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.CHARGER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.CHARGER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Coup d’épaule":"Shoulder strike";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("charger282-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
var cow=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.animal.cow.Cow>();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
var target=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);target.setPos(.5,1,13.5);p.level().addFreshEntity(target);cow.set(target);
|
||||
var mob=wild.get();mob.setPermanentlyInvulnerable(false);mob.setTarget(target);mob.setOnGround(true);mob.setNoAi(false);
|
||||
});
|
||||
c.waitFor(m->phase.get()==ChargerEntity.Phase.WINDUP,200);c.waitTicks(3);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("charger282-windup");
|
||||
c.waitFor(m->phase.get()==ChargerEntity.Phase.CHARGING,100);c.waitTicks(2);c.takeScreenshot("charger282-charging");
|
||||
c.waitFor(m->phase.get()==ChargerEntity.Phase.RECOVERING,100);
|
||||
server.runOnServer(s->check(cow.get().getHealth()==96,"The wild charger reaches actual body contact and deals four damage"));
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
wild.get().discard();wild.set(null);cow.get().discard();enemy.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(20)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("CHARGER282_CLIENT_PASS: Vulkan, adult skin, three miniature sizes, native G impact and cooldown, wild windup/charge/impact, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen;
|
||||
|
||||
/** Exercises the real startup hook and vanilla multiplayer list without opening a world. */
|
||||
public final class DefaultServer295ClientChecks implements FabricClientGameTest {
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
c.runOnClient(m -> {
|
||||
if (!com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo().backendName().equalsIgnoreCase("vulkan"))
|
||||
throw new AssertionError("Vulkan required");
|
||||
if (!java.nio.file.Files.exists(m.gameDirectory.toPath().resolve(".sanctuary/default-server-beta")))
|
||||
throw new AssertionError("Client startup hook did not initialize the default");
|
||||
});
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getWindow().setWindowed(1280, 800);
|
||||
m.options.guiScale().set(3); m.resizeGui();
|
||||
m.getLanguageManager().setSelected(locale);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.gui.setScreen(new JoinMultiplayerScreen(new TitleScreen()));
|
||||
var list = ((JoinMultiplayerScreen) m.gui.screen()).getServers();
|
||||
int count = 0;
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
var server = list.get(i);
|
||||
if (server.ip.equals(DefaultServerList.ADDRESS)) {
|
||||
count++;
|
||||
if (!server.name.equals("Sanctuary Beta") || server.getIconBytes() == null)
|
||||
throw new AssertionError("Missing default name or cached icon");
|
||||
}
|
||||
}
|
||||
if (count != 1) throw new AssertionError("Expected one Sanctuary default, got " + count);
|
||||
});
|
||||
c.waitTicks(30);
|
||||
c.takeScreenshot("default-server295-" + locale);
|
||||
c.runOnClient(m -> m.gui.setScreen(new TitleScreen()));
|
||||
}
|
||||
System.out.println("DEFAULT_SERVER295_VULKAN_PASS: startup, vanilla list and logo FR/EN; no world opened");
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.whale.*;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Real pack renderer, animated ghost and native egg under Vulkan. Fresh development world only. */
|
||||
public final class FlyingWhale290ClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
|
||||
var whaleId = new AtomicInteger();
|
||||
c.runOnClient(m -> {
|
||||
var device = com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo();
|
||||
check(device.backendName().equalsIgnoreCase("vulkan"), "Whale validation requires Vulkan: " + device.backendName());
|
||||
System.out.println("WHALE290_BACKEND: " + device.backendName() + " / " + device.name());
|
||||
m.options.renderDistance().set(8); m.options.simulationDistance().set(8); m.options.pauseOnLostFocus = false;
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
IntroClient.setQuickTestIntro(true); Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen, "sanctuary.progression.enter"); intro.set(false);
|
||||
}
|
||||
});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("FlyingWhale290-" + UUID.randomUUID()); s.setSeed("290"); s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null
|
||||
&& e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()) {
|
||||
c.waitFor(m -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
|
||||
var server = world.getServer();
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "time set noon");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
|
||||
p.getInventory().clearContent(); p.teleportTo(.5, 32, .5); p.setYRot(0); p.setXRot(-8);
|
||||
p.getAbilities().flying = true; p.onUpdateAbilities();
|
||||
p.getInventory().setItem(0, new ItemStack(FlyingWhales.SPAWN_EGG));
|
||||
p.getInventory().setItem(1, new ItemStack(Items.EGG)); p.inventoryMenu.broadcastChanges();
|
||||
p.level().setBlockAndUpdate(new BlockPos(0, 30, 0), Blocks.GLASS.defaultBlockState());
|
||||
for (int i = 0; i < 2; i++) {
|
||||
var whale = FlyingWhales.TYPE.create(p.level(), EntitySpawnReason.COMMAND);
|
||||
whale.setNoAi(true); whale.setPos(i == 0 ? -4 : 30, i == 0 ? 35 : 47, i == 0 ? 43 : 92);
|
||||
whale.setYRot(i == 0 ? 105 : 250); whale.setYHeadRot(whale.getYRot()); whale.setYBodyRot(whale.getYRot());
|
||||
p.level().addFreshEntity(whale); if (i == 0) whaleId.set(whale.getId());
|
||||
}
|
||||
});
|
||||
c.waitFor(m -> m.level.getEntity(whaleId.get()) instanceof FlyingWhaleEntity, 400);
|
||||
c.waitTicks(20);
|
||||
server.runOnServer(s -> check(FlyingWhales.countLoaded(s) == 2, "Two real whales are loaded after the player chunks arrive"));
|
||||
c.runOnClient(m -> {
|
||||
m.player.setYRot(0); m.player.setXRot(-8); m.getWindow().setWindowed(1280, 960);
|
||||
var whale = (FlyingWhaleEntity)m.level.getEntity(whaleId.get());
|
||||
check(m.getEntityRenderDispatcher().getRenderer(whale) instanceof FlyingWhaleRenderer, "Historical renderer is installed");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/flying_whale/flying_whale.png")).isPresent(), "Historical cloud texture is packaged");
|
||||
var renderer = (FlyingWhaleRenderer)m.getEntityRenderDispatcher().getRenderer(whale);
|
||||
var state = renderer.createRenderState(whale, 1);
|
||||
check((renderer.getModelTint(state) >>> 24) < 255, "The whale keeps its translucent ghost tint");
|
||||
var model = new FlyingWhaleModel(m.getEntityModels().bakeLayer(FlyingWhaleModel.LAYER_LOCATION));
|
||||
state.ageInTicks = 0; model.setupAnim(state);
|
||||
float fin = model.root().getChild("whale").getChild("left_fin").zRot;
|
||||
state.ageInTicks = 60; model.setupAnim(state);
|
||||
check(Math.abs(fin - model.root().getChild("whale").getChild("left_fin").zRot) > .01, "Fins animate on the actual model");
|
||||
});
|
||||
c.waitTicks(35); c.takeScreenshot("whale290-native-egg-and-two-whales");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1); c.waitTicks(3);
|
||||
c.takeScreenshot("whale290-ghosts");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale); m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
String name = new ItemStack(FlyingWhales.SPAWN_EGG).getHoverName().getString();
|
||||
check(!name.contains("item.sanctuary") && name.contains(locale.equals("fr_fr") ? "baleine" : "Whale"), "Native egg label is translated");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("whale290-egg-" + locale);
|
||||
}
|
||||
var start = new AtomicReference<Vec3>();
|
||||
server.runOnServer(s -> {
|
||||
var whale = (FlyingWhaleEntity)s.overworld().getEntity(whaleId.get());
|
||||
start.set(whale.position()); whale.setNoAi(false);
|
||||
});
|
||||
c.waitTicks(60);
|
||||
server.runOnServer(s -> {
|
||||
var whale = (FlyingWhaleEntity)s.overworld().getEntity(whaleId.get());
|
||||
check(whale.position().distanceToSqr(start.get()) > .01, "Historical AI flies around the real connected player");
|
||||
});
|
||||
c.takeScreenshot("whale290-flight");
|
||||
System.out.println("WHALE290_CLIENT_PASS: Vulkan, two giant translucent animated whales, native egg, FR/EN, actual historical flight");
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.creeper.HappyCreepers;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.renderer.entity.CreeperRenderer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.monster.Creeper;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Real sneak-clicks, synced flower/face, charged combination and real server AI under Vulkan. */
|
||||
public final class HappyCreeperClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); }
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable(); var intro = new AtomicBoolean(true);
|
||||
c.runOnClient(m -> {
|
||||
m.options.renderDistance().set(5); m.options.simulationDistance().set(5); m.options.pauseOnLostFocus = false;
|
||||
if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("voicechat")) {
|
||||
var voice = de.maxhenkel.voicechat.VoicechatClient.CLIENT_CONFIG;
|
||||
voice.muted.set(true); voice.disabled.set(true); voice.onboardingFinished.set(true);
|
||||
voice.macosCheckMicrophonePermission.set(false);
|
||||
}
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> { if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
IntroClient.setQuickTestIntro(true); Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen, "sanctuary.progression.enter"); intro.set(false);
|
||||
}});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("HappyCreeper-" + UUID.randomUUID()); s.setSeed("290"); s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null && e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()) {
|
||||
c.waitFor(m -> m.player != null && ProgressionClient.active() && m.gui.screen() == null && !IntroSpawnFlash.active(), 1200);
|
||||
var server = world.getServer();
|
||||
int id = server.computeOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst(); p.getAbilities().invulnerable = true;
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL, true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "time set noon");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
|
||||
p.getInventory().clearContent(); p.teleportTo(.5, 1, .5); p.setYRot(0); p.setXRot(0);
|
||||
fr.koka.sanctuary.inventory.AccessoryAccess.of(p).clearContent(); FamiliarService.tick(p);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.POPPY)); p.inventoryMenu.broadcastChanges();
|
||||
for (int x = -9; x <= 9; x++) for (int z = -3; z <= 12; z++)
|
||||
p.level().setBlockAndUpdate(new BlockPos(x, 0, z), Blocks.STONE.defaultBlockState());
|
||||
var mob = EntityTypes.CREEPER.create(p.level(), EntitySpawnReason.COMMAND); mob.setNoAi(true);
|
||||
mob.setPos(.5, 1, 2.5); mob.setYRot(180); mob.yHeadRot = mob.yBodyRot = 180; p.level().addFreshEntity(mob); return mob.getId();
|
||||
});
|
||||
c.runOnClient(m -> m.getWindow().setWindowed(1280, 960));
|
||||
c.waitFor(m -> m.level.getEntity(id) != null && m.player.getMainHandItem().is(Items.POPPY), 200);
|
||||
c.getInput().holdKey(o -> o.keyShift); c.waitTicks(5); click(c, id); c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> m.level.getEntity(id) instanceof Creeper mob && HappyCreepers.isHappy(mob) && m.player.getMainHandItem().isEmpty(), 200);
|
||||
c.runOnClient(m -> {
|
||||
var mob = (Creeper) m.level.getEntity(id); var renderer = (CreeperRenderer) m.getEntityRenderDispatcher().getRenderer(mob);
|
||||
var state = renderer.createRenderState(mob, 1); var hat = ((MobHeadState.Access) state).sanctuary$mobHat();
|
||||
check(hat.active && !hat.head.block.isEmpty(), "Exact equipped flower reaches the native head layer");
|
||||
check(renderer.getTextureLocation(state).equals(SanctuaryMod.id("textures/entity/creeper/happy.png")), "Synced flower selects the restored smile");
|
||||
check(m.getResourceManager().getResource(renderer.getTextureLocation(state)).isPresent(), "Historical happy texture is packaged");
|
||||
});
|
||||
server.runOnServer(s -> { var mob = (Creeper) s.getPlayerList().getPlayers().getFirst().level().getEntity(id); mob.setNoAi(false); });
|
||||
c.waitTicks(85);
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst(); var mob = (Creeper) p.level().getEntity(id);
|
||||
check(mob != null && mob.isAlive() && mob.getTarget() == null && !mob.isIgnited(), "Real happy AI survives next to a survival player without attacking");
|
||||
mob.setNoAi(true); mob.teleportTo(.5, 1, 2.5);
|
||||
}); c.waitTicks(5);
|
||||
c.getInput().holdKey(o -> o.keyShift); c.waitTicks(5); click(c, id); c.getInput().releaseKey(o -> o.keyShift);
|
||||
c.waitFor(m -> m.level.getEntity(id) instanceof Creeper mob && !HappyCreepers.isHappy(mob), 200);
|
||||
c.runOnClient(m -> {
|
||||
var mob = (Creeper) m.level.getEntity(id); var renderer = (CreeperRenderer) m.getEntityRenderDispatcher().getRenderer(mob);
|
||||
check(!((MobHeadState.Access) renderer.createRenderState(mob, 1)).sanctuary$mobHat().active, "Removing the flower clears its render state");
|
||||
check(!renderer.getTextureLocation(renderer.createRenderState(mob, 1)).getNamespace().equals("sanctuary"), "Removal restores the native face");
|
||||
});
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst(); check(p.getInventory().countItem(Items.POPPY) == 1, "Real empty-hand gesture returns the one flower");
|
||||
p.level().getEntity(id).discard(); p.teleportTo(.5, 1, -2.5);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
var mob = EntityTypes.CREEPER.create(p.level(), EntitySpawnReason.COMMAND); mob.setNoAi(true);
|
||||
mob.setPos((i - 1.5) * 2 + .5, 1, 4.5); mob.setYRot(180); mob.yHeadRot = mob.yBodyRot = 180;
|
||||
if (i == 1 || i == 2) HeadEquipment.set(mob, new ItemStack(i == 1 ? Items.POPPY : Items.PINK_TULIP));
|
||||
if (i >= 2) mob.thunderHit(p.level(), EntityTypes.LIGHTNING_BOLT.create(p.level(), EntitySpawnReason.COMMAND));
|
||||
mob.clearFire(); p.level().addFreshEntity(mob);
|
||||
}
|
||||
}); c.waitTicks(20);
|
||||
c.runOnClient(m -> { m.player.setYRot(0); m.player.setXRot(4);
|
||||
int happy = 0, chargedHappy = 0;
|
||||
for (var mob : m.level.getEntitiesOfClass(Creeper.class, m.player.getBoundingBox().inflate(20))) {
|
||||
var renderer = (CreeperRenderer) m.getEntityRenderDispatcher().getRenderer(mob); var state = renderer.createRenderState(mob, 1);
|
||||
if (((MobHeadState.Access) state).sanctuary$mobHat().happyCreeper) { happy++; if (state.isPowered) chargedHappy++; }
|
||||
}
|
||||
check(happy == 2 && chargedHappy == 1, "Normal, happy, charged-happy and charged states coexist on the real client");
|
||||
});
|
||||
c.getInput().pressKey(InputConstants.KEY_F1); c.waitTicks(3); c.takeScreenshot("happy-creepers-four-states"); c.getInput().pressKey(InputConstants.KEY_F1);
|
||||
System.out.println("HAPPY_CREEPER_CLIENT_PASS: Vulkan, real sneak-click equipment/removal, exact synced flower, restored face, passive server AI, independent charge");
|
||||
} finally { intro.set(false); c.getInput().releaseKey(o -> o.keyShift); }
|
||||
}
|
||||
|
||||
private static void click(ClientGameTestContext c, int id) {
|
||||
c.runOnClient(m -> {
|
||||
var e = m.level.getEntity(id); var delta = e.getBoundingBox().getCenter().subtract(m.player.getEyePosition());
|
||||
m.player.setYRot((float) Math.toDegrees(Math.atan2(-delta.x, delta.z)));
|
||||
m.player.setXRot((float) -Math.toDegrees(Math.atan2(delta.y, Math.sqrt(delta.x * delta.x + delta.z * delta.z))));
|
||||
}); c.waitTicks(3);
|
||||
c.runOnClient(m -> check(m.hitResult instanceof net.minecraft.world.phys.EntityHitResult hit && hit.getEntity().getId() == id, "Mouse actually aims at the creeper"));
|
||||
c.getInput().pressMouse(InputConstants.MOUSE_BUTTON_RIGHT); c.waitTicks(7);
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.HunterEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Hunter286ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<HunterEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var phase=new java.util.concurrent.atomic.AtomicReference<>(HunterEntity.Phase.IDLE);
|
||||
var familyAir=new AtomicBoolean();var familyPeak=new java.util.concurrent.atomic.AtomicReference<Double>(1D);var wildPeak=new java.util.concurrent.atomic.AtomicReference<Double>(1D);
|
||||
var signature=new java.util.concurrent.atomic.AtomicReference<fr.koka.sanctuary.familiar.FamiliarTechniques.Cast>();
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{
|
||||
var mob=wild.get();if(mob!=null&&!mob.isRemoved()){phase.set(mob.phase());wildPeak.set(Math.max(wildPeak.get(),mob.getY()));}
|
||||
if(s.getPlayerList().getPlayers().isEmpty())return;var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
var cast=FamiliarBattle.state(p).cast;if(cast!=null&&!cast.automatic&&cast.technique.effect().equals("pounce"))signature.compareAndSet(null,cast);
|
||||
if(pet!=null&&signature.get()!=null){familyPeak.set(Math.max(familyPeak.get(),pet.getY()));if(pet.getY()>1.4&&!pet.onGround())familyAir.set(true);}
|
||||
});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Hunter286-"+UUID.randomUUID());s.setSeed("286");s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
for(int x=-16;x<=16;x++)for(int z=-16;z<=16;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-2;x<=2;x++)for(int z:new int[]{3,7,11})p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.HUNTER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.HUNTER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:hunter"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.HUNTER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/hunter.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.HUNTER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
wild.get().teleportTo(3.5,1,4.5);var pet=FamiliarService.active(p);pet.teleportTo(.5,1,1.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,8.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);
|
||||
c.waitFor(m->familyAir.get(),250);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(1);c.takeScreenshot("hunter286-familiar-airborne");
|
||||
c.waitTicks(60);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(signature.get()!=null&&signature.get().hit.size()==1&&enemy.get().getHealth()<100&&familyPeak.get()>2,"Real G uses an airborne native body and one signature hit");
|
||||
check(pet.isAlive()&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"The miniature survives and retains its durable G cooldown");
|
||||
FamiliarBattle.command(p,"follow");wild.get().teleportTo(.5,1,4.5);enemy.get().setPermanentlyInvulnerable(true);enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("hunter286-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.HUNTER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.HUNTER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Griffure":"Claw strike";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("hunter286-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
p.getAbilities().invulnerable=false;p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);p.setHealth(6);
|
||||
var mob=wild.get();mob.teleportTo(.5,1,8.5);mob.setPermanentlyInvulnerable(false);mob.setOnGround(true);mob.setTarget(p);mob.setNoAi(false);phase.set(HunterEntity.Phase.IDLE);wildPeak.set(1D);
|
||||
});c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});
|
||||
c.waitFor(m->phase.get()==HunterEntity.Phase.WINDUP,200);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("hunter286-windup");
|
||||
c.waitFor(m->phase.get()==HunterEntity.Phase.LEAPING,150);c.waitTicks(2);c.takeScreenshot("hunter286-wild-airborne");
|
||||
c.waitFor(m->phase.get()==HunterEntity.Phase.RECOVERING,150);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(p.getHealth()<6&&p.getHealth()>0&&wildPeak.get()>2,"The wild Hunter physically jumps onto the real player and attacks");
|
||||
check(wild.get().pounceCooldown()>0&&!wild.get().doHurtTarget(p.level(),p),"Recovery prevents a second melee strike after impact");p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
});c.takeScreenshot("hunter286-contact");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(28);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(wild.get().onGround()&&!wild.get().isNoGravity(),"The Hunter lands with normal gravity");
|
||||
wild.get().discard();var mob=Infected.HUNTER.create(p.level(),EntitySpawnReason.COMMAND);mob.setPos(.5,1,8.5);mob.setOnGround(true);wild.set(mob);p.level().addFreshEntity(mob);
|
||||
p.setGameMode(net.minecraft.world.level.GameType.SURVIVAL);p.setHealth(6);p.teleportTo(.5,1,.5);p.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);p.setYRot(0);p.setXRot(0);mob.setTarget(p);phase.set(HunterEntity.Phase.IDLE);wildPeak.set(1D);
|
||||
});c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});
|
||||
c.waitFor(m->phase.get()==HunterEntity.Phase.LEAPING,250);c.getInput().holdKey(o->o.keyRight);c.waitTicks(12);c.getInput().releaseKey(o->o.keyRight);c.waitTicks(8);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(p.getHealth()==6&&Math.abs(p.getX()-.5)>.7&&wildPeak.get()>2&&wild.get().pounceCooldown()>0,"Real sideways keyboard input dodges the fixed airborne trajectory");p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("hunter286-dodged");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();wild.get().discard();wild.set(null);enemy.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(24)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("HUNTER286_CLIENT_PASS: Vulkan, adult skin, native airborne G/contact/cooldown, real wild jump/player attack/recovery/landing, real keyboard dodge, three adult miniature sizes, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Infected280ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Infected281-"+UUID.randomUUID());s.setSeed("281");s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
for(int x=-8;x<=8;x++)for(int z=-8;z<=8;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-2;x<=2;x++)p.level().setBlockAndUpdate(new BlockPos(x,0,3),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.SPAWN_EGG);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:infected"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.TYPE.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(InfectedRenderer.TEXTURE).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.SPAWN_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.UNCOMMON,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});c.waitTicks(60);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("infected281-adult-zombie");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("infected281-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.SPAWN_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.SPAWN_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Morsure":"Bite";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("infected281-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
System.out.println("INFECTED281_CLIENT_PASS: Vulkan, adult-only infected, three real miniature sizes, translated egg and FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.atlas.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.PauseScreen;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Five separated survey fixtures, real remote chunk delivery and native Vulkan rendering. */
|
||||
public final class Map279ClientChecks implements FabricClientGameTest {
|
||||
private static final int[][] ISLANDS={{0,0},{8192,0},{-8192,0},{0,8192},{0,-8192}};
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get() && m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Map279-"+UUID.randomUUID());s.setSeed("279");s.setAllowCommands(false);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null
|
||||
&& e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null && ProgressionClient.active() && m.gui.screen()==null && !IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
p.getAbilities().invulnerable=true;p.getAbilities().flying=true;p.onUpdateAbilities();p.setNoGravity(true);
|
||||
p.teleportTo(p.level(),.5,80,.5,Set.of(),0,0,true);
|
||||
var r=ProgressRecord.INITIAL.buy("atlas",1);p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");
|
||||
var store=AtlasService.observations(p);byte[] history=new byte[256];Arrays.fill(history,(byte)42);
|
||||
for(int i=0;i<16384;i++)store.observe(new AtlasStore.Tile(i,3000,history));
|
||||
for(int i=0;i<ISLANDS.length;i++) {
|
||||
byte[] colors=new byte[256];Arrays.fill(colors,(byte)((MapColor.GRASS.id+i*2)*4+2));
|
||||
for(int z=-8;z<=8;z++)for(int x=-8;x<=8;x++)store.observe(new AtlasStore.Tile(ISLANDS[i][0]/16+x,ISLANDS[i][1]/16+z,colors));
|
||||
}
|
||||
check(store.size()>16384 && !store.full(),"Old full atlas resumes surveying");
|
||||
});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas(),200);
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(2);m.resizeGui();m.gui.setScreen(new SanctuaryAtlasScreen(null));});
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen a && a.isComplete() && a.observedTiles()>0,400);
|
||||
c.runOnClient(m->zoom((SanctuaryAtlasScreen)m.gui.screen(),-23));
|
||||
c.waitFor(m->m.gui.screen() instanceof SanctuaryAtlasScreen a && a.detail()>0 && a.isComplete() && allIslands(a),500);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(a.zoom()<1./32,"Zoom passes one screen pixel per chunk");check(a.renderedPixelAt(9008,0)==0,"Remote destination is initially unknown");});
|
||||
c.takeScreenshot("map279-five-islands-overview");
|
||||
int token=c.computeOnClient(m->field(m.gui.screen(),"token",Integer.class));
|
||||
// The camera remains at the origin while native chunks arrive beyond the remote survey fixture.
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.teleportTo(p.level(),9008.5,80,.5,Set.of(),0,0,true);});
|
||||
var gold=new BlockPos(9008,100,0);
|
||||
server.runOnServer(s->s.overworld().getChunk(563,0));
|
||||
c.waitTicks(30);server.runOnServer(s->s.overworld().setBlockAndUpdate(gold,Blocks.GOLD_BLOCK.defaultBlockState()));
|
||||
c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return a.renderedPixelAt(9008,0)!=0;},200);
|
||||
for(int attempt=0;attempt<30;attempt++) {
|
||||
boolean surveyed=server.computeOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var tile=AtlasService.observations(p).get(563,0);return tile!=null && Byte.toUnsignedInt(tile.colors()[0])/4==MapColor.GOLD.id;});
|
||||
if(surveyed)break;c.waitTicks(2);
|
||||
}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var tile=AtlasService.observations(p).get(563,0);
|
||||
check(tile!=null && Byte.toUnsignedInt(tile.colors()[0])/4==MapColor.GOLD.id,"Native remote construction is surveyed after old capacity");});
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(field(a,"token",Integer.class)==token && Math.abs(a.centerX())<1,"Remote travel updates the same fixed overview stream");});
|
||||
for(String lang:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(lang);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();});
|
||||
c.waitTicks(15);c.takeScreenshot("map279-overview-"+lang+"-gui"+scale);
|
||||
c.runOnClient(m->check(allIslands((SanctuaryAtlasScreen)m.gui.screen()),"Five islands remain painted after GUI resize"));
|
||||
}
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();zoom(a,-12);});
|
||||
c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return a.zoom()<1./256 && a.isComplete();},400);
|
||||
c.takeScreenshot("map279-beyond-256-blocks-per-pixel");
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();zoom(a,-100);});
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).detail()==16 && ((SanctuaryAtlasScreen)m.gui.screen()).isComplete(),400);
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();check(Math.min(a.width,a.height)/a.zoom()>60000000,"The whole world fits both window axes");});
|
||||
c.takeScreenshot("map279-world-scale");
|
||||
c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();zoom(a,100);a.recenter();});
|
||||
c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return a.detail()==0 && a.isComplete() && a.renderedPixelAt(9008,0)==MapColor.GOLD.calculateARGBColor(MapColor.Brightness.NORMAL);},400);
|
||||
c.takeScreenshot("map279-return-to-blocks");
|
||||
c.runOnClient(m->m.gui.setScreen(new PauseScreen(true)));
|
||||
c.waitFor(m->{var map=((PauseAtlasAccess)m.gui.screen()).sanctuary$pauseMap();return map!=null && map.knownPixels()>0;},300);
|
||||
c.runOnClient(m->m.gui.setScreen(new SanctuaryAtlasScreen(null)));
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).observedTiles()>0,300);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.overworld().setChunkForced(750,0,true);s.overworld().getChunk(750,0);s.overworld().setBlockAndUpdate(new BlockPos(12000,100,0),Blocks.DIAMOND_BLOCK.defaultBlockState());
|
||||
check(!AtlasService.observations(p).contains(750,0),"Loaded operator fixture is outside personal discoveries");
|
||||
((net.minecraft.client.server.IntegratedServer)s).setWorldAllowCommands(true);
|
||||
});
|
||||
c.waitFor(m->ProgressionClient.operator && field(m.gui.screen(),"operator",Boolean.class) && ((SanctuaryAtlasScreen)m.gui.screen()).observedTiles()>0,400);
|
||||
c.runOnClient(m->zoom((SanctuaryAtlasScreen)m.gui.screen(),-25));
|
||||
try{c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return a.detail()>0 && a.isComplete() && a.renderedPixelAt(12000,0)!=0;},600);}
|
||||
catch(AssertionError e){c.runOnClient(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();System.out.println("MAP279_OPERATOR_DEBUG status="+a.status()+" detail="+a.detail()+" complete="+a.isComplete()+" tiles="+a.observedTiles()+" center="+a.centerX()+","+a.centerZ()+" operator="+field(a,"operator",Boolean.class));});server.runOnServer(s->System.out.println("MAP279_OPERATOR_LOADED="+(s.overworld().getChunkSource().getChunkNow(750,0)!=null)));throw e;}
|
||||
c.takeScreenshot("map279-operator-overview");
|
||||
server.runOnServer(s->{check(!AtlasService.observations(s.getPlayerList().getPlayers().getFirst()).contains(750,0),"Operator reduction never adds personal discoveries");((net.minecraft.client.server.IntegratedServer)s).setWorldAllowCommands(false);});
|
||||
c.waitFor(m->{var a=(SanctuaryAtlasScreen)m.gui.screen();return !ProgressionClient.operator && !field(a,"operator",Boolean.class) && a.isComplete() && a.observedTiles()>0 && a.renderedPixelAt(12000,0)==0;},400);
|
||||
server.runOnServer(s->{s.overworld().setChunkForced(750,0,false);var p=s.getPlayerList().getPlayers().getFirst();p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->((SanctuaryAtlasScreen)m.gui.screen()).observedTiles()==0,200);
|
||||
System.out.println("MAP279_CLIENT_PASS: Vulkan, expanded exploration, five separated surveys, fixed distant overview, world scale, return to exact blocks, Pause, FR/EN GUI 2/3/4, operator overview isolation and revocation");
|
||||
}finally{intro.set(false);c.runOnClient(m->m.gui.setScreen(null));}
|
||||
}
|
||||
private static boolean allIslands(SanctuaryAtlasScreen a){for(var island:ISLANDS)if(a.renderedPixelAt(island[0],island[1])==0)return false;return true;}
|
||||
private static void zoom(SanctuaryAtlasScreen a,double scroll){
|
||||
double y=field(a,"focusY",Integer.class)+field(a,"focusSize",Integer.class)/2.;
|
||||
check(a.mouseScrolled(a.width/2.,y,0,scroll),"Native map wheel accepted");
|
||||
}
|
||||
private static <T> T field(Object target,String name,Class<T> type){try{var f=target.getClass().getDeclaredField(name);f.setAccessible(true);return type.cast(f.get(target));}catch(Exception e){throw new RuntimeException(e);}}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import de.johni0702.minecraft.bobby.Bobby;
|
||||
import de.johni0702.minecraft.bobby.ext.ClientChunkCacheExt;
|
||||
import de.maxhenkel.voicechat.VoicechatClient;
|
||||
import de.maxhenkel.voicechat.gui.VoiceChatScreen;
|
||||
import de.maxhenkel.voicechat.voice.client.ClientManager;
|
||||
import dev.tr7zw.entityculling.EntityCullingModBase;
|
||||
import dev.tr7zw.entityculling.access.Cullable;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.nio.file.Files;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Full optional pack on Vulkan, with cache and culling exercised in a fresh lab world. */
|
||||
public final class Optional292ClientChecks implements FabricClientGameTest {
|
||||
private static void check(boolean value, String message) {
|
||||
if (!value) throw new AssertionError(message);
|
||||
}
|
||||
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
var intro = new AtomicBoolean(true);
|
||||
var hidden = new AtomicInteger();
|
||||
c.runOnClient(m -> {
|
||||
check(com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo().backendName()
|
||||
.equalsIgnoreCase("vulkan"), "Only Vulkan is validated");
|
||||
for (String id : new String[]{"bobby", "lithium", "entityculling", "voicechat", "sound_physics_remastered"})
|
||||
check(FabricLoader.getInstance().isModLoaded(id), "Missing pack mod " + id);
|
||||
check(VoicechatClient.CLIENT_CONFIG != null, "Voice Chat initialized with Sound Physics");
|
||||
// Test configuration only: no capture of the user's microphone.
|
||||
VoicechatClient.CLIENT_CONFIG.muted.set(true);
|
||||
VoicechatClient.CLIENT_CONFIG.disabled.set(true);
|
||||
VoicechatClient.CLIENT_CONFIG.onboardingFinished.set(true);
|
||||
VoicechatClient.CLIENT_CONFIG.macosCheckMicrophonePermission.set(false);
|
||||
m.options.renderDistance().set(10); m.options.simulationDistance().set(8);
|
||||
m.options.pauseOnLostFocus = false;
|
||||
// Fabric's client runner resets configuration before initialization.
|
||||
// Exercise Bobby's live reload in the development directory instead.
|
||||
try {
|
||||
var path = FabricLoader.getInstance().getConfigDir().resolve("bobby.conf");
|
||||
Files.writeString(path, Files.readString(path)
|
||||
.replaceAll("(?m)^view-distance-overwrite=.*$", "view-distance-overwrite=4")
|
||||
.replaceAll("(?m)^unload-delay-secs=.*$", "unload-delay-secs=0"));
|
||||
} catch (java.io.IOException error) { throw new AssertionError(error); }
|
||||
});
|
||||
c.waitFor(m -> Bobby.getInstance().getConfig().getViewDistanceOverwrite() == 4, 200);
|
||||
if (Boolean.getBoolean("sanctuary.test.optional292WhaleOnly")) {
|
||||
new FlyingWhale290ClientChecks().runTest(c);
|
||||
System.out.println("OPTIONAL292_WHALE_PASS: four optional mods with the translucent Sanctuary renderer");
|
||||
return;
|
||||
}
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m -> {
|
||||
if (intro.get() && m.gui.screen() instanceof HelloWorldScreen screen) {
|
||||
IntroClient.setQuickTestIntro(true); Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b -> b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen, "sanctuary.progression.enter"); intro.set(false);
|
||||
}
|
||||
});
|
||||
try (var world = c.worldBuilder().adjustSettings(s -> {
|
||||
s.setName("Optional292-" + UUID.randomUUID()); s.setSeed("292"); s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e -> e.preset() != null
|
||||
&& e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());
|
||||
}).create()) {
|
||||
c.waitFor(m -> m.player != null && ProgressionClient.active() && m.gui.screen() == null
|
||||
&& !IntroSpawnFlash.active(), 1200);
|
||||
var server = world.getServer();
|
||||
server.runOnServer(s -> {
|
||||
var p = s.getPlayerList().getPlayers().getFirst();
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(), "time set noon");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS, false, s);
|
||||
p.teleportTo(.5, 32, .5); p.setYRot(0); p.setXRot(0);
|
||||
p.getAbilities().flying = true; p.onUpdateAbilities();
|
||||
for (int x = -5; x <= 5; x++) for (int y = 29; y <= 39; y++)
|
||||
p.level().setBlockAndUpdate(new BlockPos(x, y, 6), Blocks.STONE.defaultBlockState());
|
||||
var pig = EntityTypes.PIG.create(p.level(), EntitySpawnReason.COMMAND);
|
||||
pig.setNoAi(true); pig.setNoGravity(true); pig.setPos(.5, 32, 10.5);
|
||||
p.level().addFreshEntity(pig); hidden.set(pig.getId());
|
||||
});
|
||||
c.waitFor(m -> m.level.getEntity(hidden.get()) != null, 400);
|
||||
c.runOnClient(m -> {
|
||||
check(Bobby.getInstance().isEnabled(), "Bobby is active in the limited-view lab");
|
||||
check(((ClientChunkCacheExt)m.level.getChunkSource()).bobby_getFakeChunkManager() != null,
|
||||
"Bobby attached its cache to the actual client chunk manager");
|
||||
m.player.setYRot(0); m.player.setXRot(0); m.getWindow().setWindowed(1280, 960);
|
||||
check(EntityCullingModBase.instance.cullTask != null, "Culling worker initialized on Vulkan");
|
||||
});
|
||||
c.waitFor(m -> ((Cullable)m.level.getEntity(hidden.get())).isCulled(), 400);
|
||||
c.takeScreenshot("optional292-occluded");
|
||||
server.runOnServer(s -> {
|
||||
for (int x = -5; x <= 5; x++) for (int y = 29; y <= 39; y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x, y, 6), Blocks.AIR.defaultBlockState());
|
||||
});
|
||||
c.waitFor(m -> !((Cullable)m.level.getEntity(hidden.get())).isCulled(), 400);
|
||||
c.takeScreenshot("optional292-visible-again");
|
||||
// Traverse beyond the short server view, then verify persisted chunk-cache data.
|
||||
server.runOnServer(s -> s.getPlayerList().getPlayers().getFirst().teleportTo(128.5, 32, .5));
|
||||
c.waitTicks(160);
|
||||
c.waitFor(m -> !((ClientChunkCacheExt)m.level.getChunkSource()).bobby_getFakeChunkManager()
|
||||
.getFakeChunks().isEmpty(), 600);
|
||||
c.runOnClient(m -> {
|
||||
var manager = ((ClientChunkCacheExt)m.level.getChunkSource()).bobby_getFakeChunkManager();
|
||||
check(manager != null && manager.getStorage() != null, "Bobby has persistent storage after traversal");
|
||||
System.out.println("OPTIONAL292_BOBBY " + manager.getDebugString());
|
||||
});
|
||||
c.waitFor(m -> ClientManager.getClient() != null && ClientManager.getClient().getConnection() != null
|
||||
&& ClientManager.getClient().getConnection().isConnected(), 600);
|
||||
for (String locale : new String[]{"fr_fr", "en_us"}) {
|
||||
c.runOnClient(m -> {
|
||||
m.getLanguageManager().setSelected(locale);
|
||||
m.getLanguageManager().onResourceManagerReload(m.getResourceManager());
|
||||
m.gui.setScreen(new VoiceChatScreen());
|
||||
check(m.gui.screen() instanceof VoiceChatScreen, "Voice Chat screen opens with Sound Physics");
|
||||
check(ClientManager.getPlayerStateManager().isMuted(), "Voice chat stays muted during testing");
|
||||
});
|
||||
c.waitTicks(3); c.takeScreenshot("optional292-voice-" + locale);
|
||||
c.runOnClient(m -> m.gui.setScreen(null));
|
||||
}
|
||||
}
|
||||
System.out.println("OPTIONAL292_CLIENT_PASS: Vulkan, four mods, Sound Physics, culling/uncover, Bobby cache, UDP connection and FR/EN voice UI");
|
||||
}
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.SmokerEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Smoker285ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<SmokerEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var familyFlying=new AtomicBoolean();var familyCaught=new AtomicBoolean();var wildFlying=new AtomicBoolean();var wildCaught=new AtomicBoolean();
|
||||
var familyWire=new java.util.concurrent.atomic.AtomicReference<fr.koka.sanctuary.infected.SmokerLassoEntity>();
|
||||
var familyAnchor=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.phys.Vec3>();
|
||||
var wildWire=new java.util.concurrent.atomic.AtomicReference<fr.koka.sanctuary.infected.SmokerLassoEntity>();
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{
|
||||
if(s.getPlayerList().getPlayers().isEmpty())return;var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
var f=pet==null?null:fr.koka.sanctuary.infected.SmokerLassoEntity.active(pet);familyWire.set(f);
|
||||
if(f!=null&&familyAnchor.get()==null)familyAnchor.set(pet.position());
|
||||
familyFlying.set(f!=null&&!f.isReeling());if(f!=null&&f.isReeling())familyCaught.set(true);
|
||||
var mob=wild.get();var w=mob==null||mob.isRemoved()?null:fr.koka.sanctuary.infected.SmokerLassoEntity.active(mob);wildWire.set(w);
|
||||
wildFlying.set(w!=null&&!w.isReeling());if(w!=null&&w.isReeling()&&w.caughtEntity()==p)wildCaught.set(true);
|
||||
});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Smoker285-"+UUID.randomUUID());s.setSeed("285");s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
for(int x=-16;x<=16;x++)for(int z=-16;z<=22;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-2;x<=2;x++)for(int z:new int[]{3,7,11,15})p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.SMOKER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.SMOKER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:smoker"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.SMOKER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/smoker.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.SMOKER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
wild.get().teleportTo(3.5,1,4.5);var pet=FamiliarService.active(p);pet.teleportTo(.5,1,1.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,10.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);
|
||||
c.waitFor(m->familyFlying.get(),250);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(enemy.get().getHealth()==100,"Releasing G does not hit before physical contact");check(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"G commits cooldown when the lasso is launched");});
|
||||
c.runOnClient(m->{var hook=m.level.getEntitiesOfClass(fr.koka.sanctuary.infected.SmokerLassoEntity.class,m.player.getBoundingBox().inflate(24)).stream().findFirst().orElseThrow();
|
||||
var renderer=(SmokerLassoRenderer)m.getEntityRenderDispatcher().getRenderer(hook);var state=renderer.createRenderState(hook,1);
|
||||
check(state.leashStates!=null&&!state.leashStates.isEmpty(),"The real client draws the native taut zipline cable");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("smoker285-familiar-lasso-flight");
|
||||
c.waitFor(m->familyCaught.get(),150);c.waitTicks(1);c.takeScreenshot("smoker285-familiar-pull");c.waitTicks(22);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(enemy.get().getHealth()<100&&enemy.get().position().distanceTo(familyAnchor.get())<2.3,"A real G impact drags the actual enemy near the miniature: hp="+enemy.get().getHealth()+" target="+enemy.get().position()+" pet="+pet.position());
|
||||
check(pet.isAlive()&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"The familiar survives and retains spent cooldown");
|
||||
FamiliarBattle.command(p,"follow");wild.get().teleportTo(.5,1,4.5);enemy.get().setPermanentlyInvulnerable(true);enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("smoker285-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.SMOKER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.SMOKER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Petit lasso":"Short lasso";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("smoker285-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
p.getAbilities().invulnerable=false;p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
var mob=wild.get();mob.teleportTo(.5,1,14.5);mob.setPermanentlyInvulnerable(false);mob.setTarget(p);mob.setOnGround(true);mob.setNoAi(false);
|
||||
});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});
|
||||
c.waitFor(m->wildFlying.get(),250);
|
||||
server.runOnServer(s->wild.get().setNoAi(true));
|
||||
c.getInput().holdKey(o->o.keyRight);c.waitTicks(12);c.getInput().releaseKey(o->o.keyRight);c.waitTicks(20);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
check(!wildCaught.get()&&Math.abs(p.getX()-.5)>.65&&p.getZ()<2&&fr.koka.sanctuary.infected.SmokerLassoEntity.active(wild.get())==null,"A real sideways keyboard input dodges the flying lasso without a guaranteed grab");
|
||||
check(wild.get().lassoCooldown()>0,"A missed wild shot still pays its cooldown");
|
||||
wild.get().discard();var mob=Infected.SMOKER.create(p.level(),EntitySpawnReason.COMMAND);mob.setPos(.5,1,14.5);mob.setOnGround(true);wild.set(mob);p.level().addFreshEntity(mob);
|
||||
p.teleportTo(.5,1,.5);p.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);p.setYRot(0);p.setXRot(0);mob.setTarget(p);wildCaught.set(false);wildFlying.set(false);
|
||||
});c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);});
|
||||
c.waitFor(m->wildFlying.get(),250);server.runOnServer(s->wild.get().setNoAi(true));
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("smoker285-wild-lasso-flight");
|
||||
c.waitFor(m->wildCaught.get(),150);c.waitTicks(1);c.takeScreenshot("smoker285-player-captured");
|
||||
double capturedZ=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getZ());
|
||||
c.getInput().holdKey(o->o.keyDown);c.getInput().holdKey(o->o.keyShift);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.getZ()>capturedZ+.5,"A successful hostile hit reels the player even against backward/sneak input");});
|
||||
c.getInput().releaseKey(o->o.keyDown);c.getInput().releaseKey(o->o.keyShift);
|
||||
c.waitFor(m->wildWire.get()==null,200);c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.distanceTo(wild.get())<2.2&&!fr.koka.sanctuary.infected.SmokerLassoEntity.pulled(p)&&!p.isNoGravity(),"The real player arrives near the Smoker, releases the cable and keeps normal gravity");});
|
||||
c.runOnClient(m->check(m.player.getZ()>11,"The rendered player travels with the authoritative zipline"));
|
||||
c.takeScreenshot("smoker285-player-arrival");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
wild.get().discard();wild.set(null);enemy.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.SmokerLassoEntity.class,p.getBoundingBox().inflate(32)).forEach(Entity::discard);
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(32)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("SMOKER285_CLIENT_PASS: Vulkan, native cable renderer, real G physical hit and pull, real keyboard dodge, hostile player zipline against input and arrival, three adult miniature sizes, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarService;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarEntity;
|
||||
import fr.koka.sanctuary.cosmetics.FamiliarSize;
|
||||
import fr.koka.sanctuary.familiar.FamiliarBattle;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.Infected;
|
||||
import fr.koka.sanctuary.infected.SpitterEntity;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** The custom renderer and profile must also work through ordinary familiar UI paths. */
|
||||
public final class Spitter283ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var wild=new java.util.concurrent.atomic.AtomicReference<SpitterEntity>();
|
||||
var enemy=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.monster.zombie.Zombie>();
|
||||
var preparing=new AtomicBoolean();
|
||||
var cloud=new java.util.concurrent.atomic.AtomicReference<fr.koka.sanctuary.infected.AcidZoneEntity>();
|
||||
net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents.END_SERVER_TICK.register(s->{var mob=wild.get();if(mob!=null&&!mob.isRemoved()){
|
||||
preparing.set(mob.windingUp());cloud.set(fr.koka.sanctuary.infected.AcidZoneEntity.active(mob));
|
||||
|
||||
}});
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Spitter283-"+UUID.randomUUID());s.setSeed("283");s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set midnight");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary weather vanilla");
|
||||
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"weather clear");
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.teleportTo(.5,1,.5);p.setYRot(0);p.setXRot(0);
|
||||
for(int x=-16;x<=16;x++)for(int z=-16;z<=16;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-2;x<=2;x++)p.level().setBlockAndUpdate(new BlockPos(x,0,3),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
var egg=new ItemStack(Infected.SPITTER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.getInventory().setItem(0,egg.copy());p.inventoryMenu.broadcastChanges();
|
||||
var adult=Infected.SPITTER.create(p.level(),EntitySpawnReason.COMMAND);adult.setNoAi(true);adult.setPermanentlyInvulnerable(true);wild.set(adult);adult.setPos(.5,1,4.5);adult.setYRot(180);adult.yHeadRot=180;p.level().addFreshEntity(adult);
|
||||
var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);zombie.setNoAi(true);zombie.setPermanentlyInvulnerable(true);enemy.set(zombie);zombie.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);zombie.setHealth(100);zombie.setPos(-1.5,1,4.5);zombie.setYRot(180);zombie.yHeadRot=180;p.level().addFreshEntity(zombie);
|
||||
var red=Infected.TYPE.create(p.level(),EntitySpawnReason.COMMAND);red.setNoAi(true);red.setPermanentlyInvulnerable(true);red.setPos(-3.5,1,4.5);red.setYRot(180);red.yHeadRot=180;p.level().addFreshEntity(red);
|
||||
});
|
||||
c.waitFor(m->FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals("sanctuary:spitter"),400);c.waitTicks(15);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(FamiliarService.active(p)!=null&&!FamiliarService.active(p).babyAppearance(),"The integrated server equips an adult miniature");FamiliarBattle.send(p,"");});
|
||||
c.runOnClient(m->{m.player.setYRot(0);m.player.setXRot(0);m.getWindow().setWindowed(1280,960);
|
||||
var mob=Infected.SPITTER.create(m.level,EntitySpawnReason.MOB_SUMMONED);
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Custom renderer is installed for adults and familiar proxies");
|
||||
mob.setBaby(true);check(!mob.isBaby(),"The render proxy rejects native baby proportions");
|
||||
check(m.getResourceManager().getResource(SanctuaryMod.id("textures/entity/spitter.png")).isPresent(),"Custom skin is packaged");
|
||||
check(CompanionTooltips.rarity(new ItemStack(Infected.SPITTER_EGG))==fr.koka.sanctuary.companion.CompanionType.Rarity.RARE,"The new profile supplies tooltip rarity without a legacy enum");
|
||||
m.player.getInventory().setSelectedSlot(1);
|
||||
});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();FamiliarBattle.command(p,"follow");
|
||||
var pet=FamiliarService.active(p);pet.teleportTo(.5,1,1.5);pet.setOnGround(true);
|
||||
enemy.get().teleportTo(.5,1,6.5);enemy.get().setPermanentlyInvulnerable(false);
|
||||
});
|
||||
c.runOnClient(m->{var key=net.minecraft.client.KeyMapping.get("sanctuary.companion.key");key.setKey(com.mojang.blaze3d.platform.InputConstants.Type.KEYBOARD.getOrCreate(com.mojang.blaze3d.platform.InputConstants.KEY_G));net.minecraft.client.KeyMapping.resetMapping();});
|
||||
c.waitTicks(8);c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_G);c.waitTicks(55);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var zone=fr.koka.sanctuary.infected.AcidZoneEntity.active(FamiliarService.active(p));
|
||||
check(zone!=null&&!zone.isRemoved()&&zone instanceof AreaEffectCloud,"G creates a real native potion-style cloud");
|
||||
check(enemy.get().getHealth()<100&&enemy.get().getActiveEffects().isEmpty(),"Only the grounded zone damages without poison or slowness");
|
||||
check(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&FamiliarBattle.state(p).cast==null,"G commits cooldown when the projectile is released");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("spitter283-acid-zone");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
var healthAfterExit=new java.util.concurrent.atomic.AtomicReference<Float>();
|
||||
server.runOnServer(s->{healthAfterExit.set(enemy.get().getHealth());enemy.get().teleportTo(-3.5,1,6.5);});c.waitTicks(25);
|
||||
server.runOnServer(s->{check(enemy.get().getHealth()==healthAfterExit.get(),"Leaving the field stops subsequent damage immediately");
|
||||
enemy.get().setPermanentlyInvulnerable(true);enemy.get().teleportTo(-1.5,1,4.5);
|
||||
});
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.anyMatch(pet->FamiliarSize.value(pet.egg())==value),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);
|
||||
check(pet!=null&&!pet.babyAppearance(),"Every size keeps the adult form");
|
||||
pet.setNoAi(true);pet.teleportTo(2.5,1,4.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;pet.setDeltaMovement(net.minecraft.world.phys.Vec3.ZERO);
|
||||
});c.waitTicks(8);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(12)).stream()
|
||||
.filter(e->FamiliarSize.value(e.egg())==value).findFirst().orElseThrow();
|
||||
var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);
|
||||
var state=renderer.createRenderState(pet,1);
|
||||
check(state.appearance instanceof net.minecraft.client.renderer.entity.state.ZombieRenderState zombie&&!zombie.isBaby,"The real familiar renderer uses adult geometry");
|
||||
check(Math.abs(state.size*state.appearance.boundingBoxHeight-.85*value/1000)<.001,"The displayed familiar preserves its size multiplier");
|
||||
});
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(3);c.takeScreenshot("spitter283-miniature-"+value);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
}
|
||||
for(String locale:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getLanguageManager().setSelected(locale);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.options.guiScale().set(scale);m.resizeGui();
|
||||
check(!new ItemStack(Infected.SPITTER_EGG).getHoverName().getString().contains("item.sanctuary"),"Egg label is translated");
|
||||
var tooltip=new ItemStack(Infected.SPITTER_EGG).getTooltipLines(Item.TooltipContext.of(m.level),m.player,TooltipFlag.NORMAL);
|
||||
String bite=locale.equals("fr_fr")?"Petite flaque acide":"Small acid pool";
|
||||
check(tooltip.stream().anyMatch(line->line.getString().contains(bite)),"Native tooltip callback includes the custom familiar technique in both languages");
|
||||
m.gui.setScreen(new FamiliarScreen(null));
|
||||
});c.waitTicks(4);c.takeScreenshot("spitter283-familiar-"+locale+"-gui"+scale);
|
||||
}
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
var cow=new java.util.concurrent.atomic.AtomicReference<net.minecraft.world.entity.animal.cow.Cow>();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setGameMode(net.minecraft.world.level.GameType.CREATIVE);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
var target=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);target.setPos(.5,1,10.5);p.level().addFreshEntity(target);cow.set(target);
|
||||
var mob=wild.get();mob.setPermanentlyInvulnerable(false);mob.setTarget(target);mob.setOnGround(true);mob.setNoAi(false);
|
||||
});
|
||||
c.waitFor(m->preparing.get(),200);c.waitTicks(2);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("spitter283-windup");
|
||||
c.waitFor(m->cloud.get()!=null&&!cloud.get().isRemoved(),100);c.waitTicks(25);
|
||||
server.runOnServer(s->check(cow.get().getHealth()<100&&cow.get().getActiveEffects().isEmpty(),"Wild projectile creates a damaging zone without status effects"));
|
||||
c.takeScreenshot("spitter283-wild-zone");
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);
|
||||
var zone=fr.koka.sanctuary.infected.AcidZoneEntity.active(wild.get());if(zone!=null)zone.discard();
|
||||
wild.get().discard();wild.set(null);cow.get().discard();enemy.get().discard();
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.AcidSpitEntity.class,p.getBoundingBox().inflate(24)).forEach(Entity::discard);
|
||||
p.level().getEntitiesOfClass(fr.koka.sanctuary.infected.InfectedEntity.class,p.getBoundingBox().inflate(20)).forEach(Entity::discard);
|
||||
});c.waitTicks(20);
|
||||
System.out.println("SPITTER283_CLIENT_PASS: Vulkan, adult skin, three miniature sizes, real G projectile/cloud/damage/exit/cooldown, wild windup/cloud, FR/EN GUI 2/3/4");
|
||||
}
|
||||
}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Real Vulkan models, stable wardrobes, native baby UVs, native forward arms and Hunter locomotion. */
|
||||
public final class Zombie287ClientChecks implements FabricClientGameTest {
|
||||
private static final List<EntityType<? extends Zombie>> TYPES=List.of(EntityTypes.ZOMBIE,Infected.TYPE,Infected.HUNTER,Infected.BOOMER,Infected.CHARGER,Infected.SPITTER,Infected.SMOKER);
|
||||
private static final List<Item> EGGS=List.of(Items.ZOMBIE_SPAWN_EGG,Infected.SPAWN_EGG,Infected.HUNTER_EGG,Infected.BOOMER_EGG,Infected.CHARGER_EGG,Infected.SPITTER_EGG,Infected.SMOKER_EGG);
|
||||
private static final List<String> NAMES=List.of("Zombie","Infecté","Hunter","Boomer","Charger","Spitter","Smoker");
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
private static ItemStack sized(ItemStack egg,int size){
|
||||
var result=egg.copy();var tag=new net.minecraft.nbt.CompoundTag();tag.putInt("schema",1);tag.putInt("per_mille",size);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,result,t->t.put(FamiliarSize.KEY,tag));return result;
|
||||
}
|
||||
private static InfectedRenderer.State state(net.minecraft.client.Minecraft m,Zombie mob){
|
||||
check(m.getEntityRenderDispatcher().getRenderer(mob) instanceof InfectedRenderer,"Ordinary zombies and every infected share the new renderer");
|
||||
return ((InfectedRenderer)m.getEntityRenderDispatcher().getRenderer(mob)).createRenderState(mob,1);
|
||||
}
|
||||
private static void posture(InfectedRenderer.State s,boolean hunter) {
|
||||
var model=new net.minecraft.client.model.monster.zombie.ZombieModel<InfectedRenderer.State>(net.minecraft.client.Minecraft.getInstance().getEntityModels().bakeLayer(net.minecraft.client.model.geom.ModelLayers.ZOMBIE));
|
||||
s.walkAnimationSpeed=.6F;s.walkAnimationPos=2;s.swingAnimation=0;model.setupAnim(s);
|
||||
check(model.rightArm.xRot< -.5&&model.leftArm.xRot< -.5,"Every zombie keeps both native arms forward");
|
||||
if(hunter)check(s.isCrouching&&model.body.xRot>.4,"Hunter keeps its crouched walking skeleton");
|
||||
else check(!s.isCrouching&&Math.abs(model.body.xRot)<.01,"Other roles retain their native standing posture");
|
||||
float leg=model.rightLeg.xRot;s.walkAnimationPos=5;model.setupAnim(s);
|
||||
check(Math.abs(model.rightLeg.xRot-leg)>.1,"Role silhouettes retain native leg locomotion");
|
||||
}
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
var staged=new ArrayList<Zombie>();var intro=new AtomicBoolean(true);var hunterId=new AtomicInteger(-1);
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
c.runOnClient(m->{m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
press(screen,"sanctuary.progression.enter");intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Zombies287-"+UUID.randomUUID());s.setSeed("287");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();s.setDifficulty(net.minecraft.world.Difficulty.NORMAL,true);
|
||||
for(String command:List.of("sanctuary time vanilla","time set midnight","sanctuary weather vanilla","weather clear"))s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),command);
|
||||
p.level().getGameRules().set(GameRules.SPAWN_MOBS,false,s);p.getAbilities().invulnerable=true;p.teleportTo(.5,1,-2.5);p.getInventory().clearContent();
|
||||
for(int x=-26;x<=26;x++)for(int z=-16;z<=26;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=-14;x<=14;x++)for(int z:new int[]{6,10,14})p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.SEA_LANTERN.defaultBlockState());
|
||||
for(int n=0;n<9;n++){var mob=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);mob.setUUID(new UUID(0,n));mob.setNoAi(true);mob.setPermanentlyInvulnerable(true);mob.setPos((n-4)*2+.5,1,8.5);mob.setYRot(180);mob.yHeadRot=180;mob.yBodyRot=180;p.level().addFreshEntity(mob);staged.add(mob);}
|
||||
});c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.player.setYRot(0);m.player.setXRot(0);});c.waitTicks(20);
|
||||
c.runOnClient(m->{var found=new HashSet<Integer>();
|
||||
for(var type:TYPES)for(int i=0;i<9;i++){
|
||||
var mob=type.create(m.level,EntitySpawnReason.COMMAND);mob.setId(100000+i);mob.setUUID(new UUID(0,i));mob.setOnGround(true);var a=state(m,mob);
|
||||
check(a.skin==i,"Every role can wear all nine supplied skins");found.add(a.skin);var renderer=(InfectedRenderer)m.getEntityRenderDispatcher().getRenderer(mob);
|
||||
check(renderer.getTextureLocation(a).equals(ZombieSkins.texture(i,false))&&m.getResourceManager().getResource(renderer.getTextureLocation(a)).isPresent(),"The selected adult texture is available");
|
||||
mob.tickCount+=200;mob.setPos(5,2,8);check(state(m,mob).skin==i,"Time/movement do not reroll clothing");
|
||||
var clone=type.create(m.level,EntitySpawnReason.LOAD);clone.setId(100100+i);clone.setUUID(mob.getUUID());check(state(m,clone).skin==i,"Reloaded identity retains its clothing");
|
||||
}
|
||||
check(found.size()==9,"All nine variants are reachable");
|
||||
});c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("zombie287-nine-skins");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();staged.forEach(Entity::discard);staged.clear();
|
||||
for(int i=0;i<TYPES.size();i++){var mob=TYPES.get(i).create(p.level(),EntitySpawnReason.COMMAND);mob.setUUID(new UUID(10,i));mob.setNoAi(true);mob.setPermanentlyInvulnerable(true);mob.setPos((i-3)*2.4+.5,1,8.5);mob.setYRot(180);mob.yHeadRot=180;mob.yBodyRot=180;mob.setCustomName(Component.literal(NAMES.get(i)));mob.setCustomNameVisible(true);p.level().addFreshEntity(mob);staged.add(mob);}
|
||||
var baby=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);baby.setBaby(true);baby.setNoAi(true);baby.setPos(9.5,1,8.5);baby.setYRot(180);baby.yHeadRot=180;p.level().addFreshEntity(baby);staged.add(baby);
|
||||
});c.waitTicks(18);
|
||||
c.runOnClient(m->{for(int i=0;i<TYPES.size();i++){final int at=i;var mob=m.level.getEntitiesOfClass(Zombie.class,m.player.getBoundingBox().inflate(30)).stream().filter(e->e.getUUID().equals(new UUID(10,at))).findFirst().orElseThrow();var a=state(m,mob);posture(a,i==2);}
|
||||
var baby=m.level.getEntitiesOfClass(Zombie.class,m.player.getBoundingBox().inflate(30)).stream().filter(Zombie::isBaby).findFirst().orElseThrow();var a=state(m,baby);check(a.isBaby&&m.getResourceManager().getResource(ZombieSkins.texture(a.skin,true)).isPresent(),"Ordinary baby has its native model and adapted UV texture");
|
||||
});c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(2);c.takeScreenshot("zombie287-role-postures");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{staged.forEach(Entity::discard);staged.clear();s.getPlayerList().getPlayers().getFirst().teleportTo(.5,1,-6.5);});
|
||||
// No nearby monsters during wardrobe checks: changing species must not enter combat.
|
||||
for(int i=0;i<EGGS.size();i++)for(int size:new int[]{500,1000,1500}){
|
||||
final int at=i;server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var current=FamiliarBattle.egg(p);var egg=!current.isEmpty()&¤t.is(EGGS.get(at))?current:new ItemStack(EGGS.get(at));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,sized(egg,size));FamiliarService.tick(p);p.inventoryMenu.broadcastChanges();});
|
||||
c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(15)).stream().anyMatch(e->e.egg().is(EGGS.get(at))&&FamiliarSize.value(e.egg())==size),200);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var pet=FamiliarService.active(p);check(pet!=null,"Each egg equips a familiar");pet.setNoAi(true);pet.teleportTo(.5,1,-2.5);pet.setYRot(180);pet.yHeadRot=180;pet.yBodyRot=180;});c.waitTicks(4);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(15)).stream().filter(e->e.egg().is(EGGS.get(at))).findFirst().orElseThrow();var renderer=(FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet);var render=renderer.createRenderState(pet,1);var a=(InfectedRenderer.State)render.appearance;
|
||||
check(a.isBaby==(at==0),"Infected familiars keep adult geometry at every scale");check(a.skin==ZombieSkins.index(FamiliarIdentity.get(pet.egg())),"Familiar clothing uses its persistent bond identity");if(at>0)posture(a,at==2);
|
||||
});
|
||||
if(size==1000){c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.waitTicks(1);c.takeScreenshot("zombie287-familiar-"+NAMES.get(at));c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);}
|
||||
}
|
||||
var remembered=new AtomicReference<UUID>();var savedEgg=new AtomicReference<ItemStack>();
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();savedEgg.set(FamiliarBattle.egg(p).copy());remembered.set(FamiliarIdentity.get(savedEgg.get()));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);});c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,savedEgg.get());FamiliarService.tick(p);});c.waitFor(m->m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(15)).stream().anyMatch(e->remembered.get().equals(FamiliarIdentity.get(e.egg()))),200);
|
||||
server.runOnServer(s->FamiliarService.active(s.getPlayerList().getPlayers().getFirst()).setNoAi(true));c.waitTicks(5);
|
||||
c.runOnClient(m->{var pet=m.level.getEntitiesOfClass(FamiliarEntity.class,m.player.getBoundingBox().inflate(15)).stream().findFirst().orElseThrow();var a=(InfectedRenderer.State)((FamiliarRenderer)m.getEntityRenderDispatcher().getRenderer(pet)).createRenderState(pet,1).appearance;check(a.skin==ZombieSkins.index(remembered.get()),"Re-equipping the same individual retains clothing");});
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);p.teleportTo(.5,1,-6.5);p.getAbilities().invulnerable=false;
|
||||
var mob=Infected.HUNTER.create(p.level(),EntitySpawnReason.COMMAND);mob.setPos(.5,1,10.5);mob.setOnGround(true);p.level().addFreshEntity(mob);mob.setTarget(p);hunterId.set(mob.getId());staged.add(mob);
|
||||
});c.waitFor(m->{var e=m.level.getEntity(hunterId.get());return e instanceof HunterEntity h&&state(m,h).isCrouching&&state(m,h).walkAnimationSpeed>.05;},250);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("zombie287-hunter-walking-sneak");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
c.waitFor(m->m.level.getEntity(hunterId.get()) instanceof HunterEntity h&&h.getY()>1.5&&!state(m,h).isCrouching,300);
|
||||
c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);c.takeScreenshot("zombie287-hunter-jumping");c.getInput().pressKey(com.mojang.blaze3d.platform.InputConstants.KEY_F1);
|
||||
server.runOnServer(s->{staged.forEach(Entity::discard);staged.clear();});c.waitTicks(20);
|
||||
System.out.println("ZOMBIE287_CLIENT_PASS: Vulkan, nine shared stable skins, normal zombie/native baby UV, native forward arms for all seven types, all familiar roles at three sizes, persistent re-equip wardrobe, real walking Hunter crouch and airborne standing jump");
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.*;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.client.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.*;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.CameraType;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
|
||||
/** GPU brightness on real terrain, independent of the PBR response or held-item pixels. */
|
||||
public final class HandLight283ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c) {
|
||||
fr.koka.sanctuary.testing.QuickTestWorld.enable();
|
||||
var intro=new AtomicBoolean(true);
|
||||
c.runOnClient(m->{
|
||||
check(com.mojang.blaze3d.systems.RenderSystem.getDevice().getDeviceInfo().backendName().equalsIgnoreCase("vulkan"),"Vulkan required");
|
||||
m.options.renderDistance().set(5);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;
|
||||
m.options.bobView().set(false);m.options.setCameraType(CameraType.FIRST_PERSON);
|
||||
m.getWindow().setWindowed(1280,800);VanillaLight.setEnabled(false);
|
||||
});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
|
||||
IntroClient.setQuickTestIntro(true);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast)
|
||||
.filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter"))
|
||||
.findFirst().orElseThrow().onPress(enter());intro.set(false);
|
||||
}});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("HandLight283-"+UUID.randomUUID());s.setSeed("283");s.setAllowCommands(true);
|
||||
s.setGameMode(WorldCreationUiState.SelectedGameMode.CREATIVE);
|
||||
s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->m.player!=null&&ProgressionClient.active()&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{
|
||||
command(s,"sanctuary time vanilla");command(s,"sanctuary weather vanilla");command(s,"time set midnight");command(s,"weather clear");
|
||||
s.getGlobalGameRules().set(GameRules.ADVANCE_TIME,false,s);s.getGlobalGameRules().set(GameRules.SPAWN_MOBS,false,s);
|
||||
var p=s.getPlayerList().getPlayers().getFirst();p.getInventory().clearContent();
|
||||
for(int x=-10;x<=10;x++)for(int z=-10;z<=10;z++)for(int y=99;y<=107;y++)
|
||||
s.overworld().setBlockAndUpdate(new BlockPos(x,y,z),(y==99||y==107||Math.abs(x)==10||Math.abs(z)==10?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
p.teleportTo(.5,100,.5);
|
||||
});
|
||||
c.waitFor(m->m.player.getY()>99,200);look(c,server,0,35);c.waitTicks(60);
|
||||
c.getInput().pressKey(InputConstants.KEY_F1);settle(c);
|
||||
var dark=capture(c,"hand283-dark");double darkMean=mean(dark);
|
||||
equip(c,server,EquipmentSlot.MAINHAND,Items.TORCH);var torch=capture(c,"hand283-main-torch");
|
||||
double torchMean=mean(torch);System.out.println("HAND283_BRIGHTNESS dark="+darkMean+" torch="+torchMean);
|
||||
check(torchMean>darkMean+12,"Held torch must brighten terrain with PBR and shader OFF: "+darkMean+" -> "+torchMean);
|
||||
Runnable pinned=FabricLoader.getInstance().isModLoaded("sodium")?SodiumHandLight283Checks.capture(c):()->{};
|
||||
for(boolean ao:new boolean[]{false,true}) {
|
||||
c.runOnClient(m->{m.options.ambientOcclusion().set(ao);m.levelExtractor.allChanged();});settle(c);
|
||||
check(mean(capture(c,"hand283-ao-"+ao))>darkMean+12,"Both flat and smooth terrain receive hand light");
|
||||
}
|
||||
equip(c,server,EquipmentSlot.MAINHAND,null);check(Math.abs(mean(capture(c,null))-darkMean)<2,"Removing torch restores darkness");
|
||||
pinned.run();
|
||||
equip(c,server,EquipmentSlot.OFFHAND,Items.LANTERN);check(mean(capture(c,"hand283-offhand-lantern"))>darkMean+12,"Offhand lantern brightens terrain");
|
||||
c.runOnClient(m->{VanillaLight.setProfile(ShaderProfile.MEDIUM);VanillaLight.setEnabled(true);VanillaLight.setPbr(false);});settle(c);
|
||||
check(mean(capture(c,"hand283-medium-pbr-off"))>darkMean+12,"Medium shader keeps actual light without PBR");
|
||||
c.runOnClient(m->VanillaLight.setPbr(true));settle(c);check(mean(capture(c,"hand283-medium-pbr-on"))>darkMean+12,"PBR keeps terrain illumination");
|
||||
c.runOnClient(m->VanillaLight.setEnabled(false));equip(c,server,EquipmentSlot.OFFHAND,null);
|
||||
check(Math.abs(mean(capture(c,"hand283-removed"))-darkMean)<2,"No stale light remains");
|
||||
equip(c,server,EquipmentSlot.MAINHAND,Items.TORCH);
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(8.5,100,.5));c.waitTicks(12);settle(c);
|
||||
c.runOnClient(m->{var lights=DynamicLightClient.snapshot(m.level);
|
||||
check((lights.apply(0,8.5,101.5,.5)&255)>200,"Source follows the moving holder");
|
||||
check((lights.apply(0,.5,101.5,.5)&255)<120,"Old position no longer has full brightness");});
|
||||
server.runOnServer(s->s.getPlayerList().getPlayers().getFirst().teleportTo(.5,100,.5));
|
||||
equip(c,server,EquipmentSlot.MAINHAND,null);
|
||||
server.runOnServer(s->{var drop=new net.minecraft.world.entity.item.ItemEntity(s.overworld(),.5,100,2.5,new ItemStack(Items.TORCH));
|
||||
drop.setNoGravity(true);drop.setPickUpDelay(32767);s.overworld().addFreshEntity(drop);});c.waitTicks(12);settle(c);
|
||||
check(mean(capture(c,"hand283-dropped"))>darkMean+12,"Dropped sources use the same terrain bridge");
|
||||
server.runOnServer(s->{var drops=new java.util.ArrayList<net.minecraft.world.entity.Entity>();s.overworld().getAllEntities().forEach(e->{if(e instanceof net.minecraft.world.entity.item.ItemEntity)drops.add(e);});drops.forEach(net.minecraft.world.entity.Entity::discard);});
|
||||
c.waitTicks(12);settle(c);check(Math.abs(mean(capture(c,null))-darkMean)<2,"Removing dropped source restores darkness");
|
||||
c.runOnClient(m->{check(m.level.getBrightness(LightLayer.BLOCK,new BlockPos(0,101,2))==0,"World light engine remains untouched");
|
||||
check(DynamicLightClient.snapshot(m.level).size()==0,"Sources are cleared");});
|
||||
System.out.println("HAND283_PASS sodium="+FabricLoader.getInstance().isModLoaded("sodium")+" dark="+darkMean+" torch="+torchMean+" Vulkan hands, AO, shader, PBR, movement, drop, cleanup");
|
||||
} finally {intro.set(false);}
|
||||
c.runOnClient(m->check(DynamicLightClient.pendingSections()==0,"Disconnect clears pending sections"));
|
||||
}
|
||||
private static void equip(ClientGameTestContext c,TestServerContext server,EquipmentSlot slot,Item item) {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setItemSlot(slot,item==null?ItemStack.EMPTY:new ItemStack(item));p.inventoryMenu.broadcastChanges();});
|
||||
c.waitTicks(12);settle(c);
|
||||
}
|
||||
private static net.minecraft.client.input.KeyEvent enter() {return new net.minecraft.client.input.KeyEvent(InputConstants.KEY_RETURN,0,0);}
|
||||
private static void settle(ClientGameTestContext c) {
|
||||
c.waitFor(m->DynamicLightClient.pendingSections()==0&&m.levelRenderer.hasRenderedAllSections(),1200);c.waitTicks(25);
|
||||
}
|
||||
private static double mean(Frame frame) {
|
||||
double sum=0;int count=0;
|
||||
// Centre-left floor, away from GUI, arm, item and room edges.
|
||||
for(int y=frame.height()/3;y<frame.height()*2/3;y++)for(int x=frame.width()/4;x<frame.width()/2;x++) {
|
||||
int pixel=frame.pixels()[y*frame.width()+x];sum+=.2126*(pixel>>16&255)+.7152*(pixel>>8&255)+.0722*(pixel&255);count++;
|
||||
}
|
||||
return sum/count;
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package fr.koka.sanctuary.client.shader;
|
||||
|
||||
import static fr.koka.sanctuary.client.shader.PixelShadows122ClientChecks.check;
|
||||
import fr.koka.sanctuary.client.DynamicLightRegion;
|
||||
import fr.koka.sanctuary.lighting.DynamicLightField;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.caffeinemc.mods.sodium.client.world.LevelSlice;
|
||||
import net.caffeinemc.mods.sodium.client.world.cloned.ClonedChunkSectionCache;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
|
||||
/** Loaded only with the optional exact pack dependency. Exercise late/reused worker jobs. */
|
||||
final class SodiumHandLight283Checks {
|
||||
private static final BlockPos PROBE=new BlockPos(0,101,2);
|
||||
static Runnable capture(ClientGameTestContext c) {
|
||||
var context=c.computeOnClient(m->LevelSlice.prepare(m.level,SectionPos.of(PROBE),new ClonedChunkSectionCache(m.level)));
|
||||
var snapshot=((DynamicLightRegion)context).sanctuary$lightSnapshot();
|
||||
check(snapshot.size()==1,"Sodium job captures the hand source");
|
||||
var slice=c.computeOnClient(m->new LevelSlice(m.level));
|
||||
return ()->{
|
||||
var fresh=c.computeOnClient(m->LevelSlice.prepare(m.level,SectionPos.of(PROBE),new ClonedChunkSectionCache(m.level)));
|
||||
var future=CompletableFuture.runAsync(()->{
|
||||
slice.copyData(context);
|
||||
check(((DynamicLightRegion)(Object)slice).sanctuary$lightSnapshot()==snapshot,"Late worker keeps its submitted immutable snapshot");
|
||||
check(slice.getBrightness(LightLayer.BLOCK,PROBE)>10,"Delayed job stays lit after live source removal");
|
||||
check(slice.getBrightness(LightLayer.SKY,PROBE)==0,"Sodium sky light remains unchanged");
|
||||
slice.copyData(fresh);
|
||||
check(slice.getBrightness(LightLayer.BLOCK,PROBE)==0,"Reused worker replaces old source data");
|
||||
slice.reset();
|
||||
check(((DynamicLightRegion)(Object)slice).sanctuary$lightSnapshot()==DynamicLightField.Snapshot.EMPTY,"Reset releases the snapshot");
|
||||
});
|
||||
c.waitFor(m->future.isDone(),200);future.join();
|
||||
System.out.println("HAND283_SODIUM_SNAPSHOT_PASS late worker, replacement, sky, reset");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,18 @@ public final class Atlas244GameTests {
|
||||
&& h.getLevel().getChunkSource().getChunkNow(far,far)==null,"No chunk ticket or terrain generation for the atlas");
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void deliveredTerrainContinuesBeyondOldCapacity(GameTestHelper h) {
|
||||
var player=inhabitant(h,GameType.CREATIVE);var store=AtlasService.observations(player);
|
||||
byte[] history=new byte[256];Arrays.fill(history,(byte)42);
|
||||
for(int i=0;i<16384;i++)store.observe(new AtlasStore.Tile(i,1000,history));
|
||||
var origin=h.absolutePos(BlockPos.ZERO);
|
||||
var chunk=h.getLevel().getChunkSource().getChunkNow(origin.getX()>>4,origin.getZ()>>4);
|
||||
h.assertTrue(!store.full(),"Old full atlas can record new islands");
|
||||
AtlasService.observeSentChunk(player,chunk);
|
||||
h.assertTrue(store.size()>16384 && store.contains(chunk.getPos().x(),chunk.getPos().z()),"Native sent chunk is recorded after the old ceiling");
|
||||
for(int altitude:AtlasAltitude.LEVELS)h.assertTrue(AtlasService.observations(player,altitude).contains(chunk.getPos().x(),chunk.getPos().z()),"All six layers still survey delivered terrain");
|
||||
h.succeed();
|
||||
}
|
||||
private static ServerPlayer inhabitant(GameTestHelper h,GameType mode) {
|
||||
var p=(ServerPlayer)h.makeMockServerPlayer(mode);
|
||||
p.setAttached(ProgressionService.RECORD,ProgressRecord.INITIAL.json());ProgressEffects.apply(p,ProgressRecord.INITIAL);return p;
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.animal.cow.Cow;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native cone, death splash and temporary diversion; familiar permission and miniature paths. */
|
||||
public final class Boomer284GameTests {
|
||||
private static final class StillZombie extends Zombie {
|
||||
StillZombie(net.minecraft.world.level.Level level){super(EntityTypes.ZOMBIE,level);}
|
||||
@Override protected void registerGoals() { }
|
||||
}
|
||||
private static BlockPos arena(GameTestHelper h,int slot) {
|
||||
var at=h.absolutePos(new BlockPos(0,174+slot*11,0));
|
||||
for(int x=-8;x<=8;x++)for(int z=-6;z<=15;z++) {
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,-1,z),Blocks.STONE.defaultBlockState());
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,7,z),Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
return at;
|
||||
}
|
||||
private static BoomerEntity boomer(GameTestHelper h,BlockPos at) {
|
||||
var source=Infected.BOOMER.create(h.getLevel(),EntitySpawnReason.COMMAND);source.setNoAi(true);source.setOnGround(true);
|
||||
source.setPos(Vec3.atBottomCenterOf(at));h.getLevel().addFreshEntity(source);return source;
|
||||
}
|
||||
private static Cow cow(GameTestHelper h,Vec3 at) {
|
||||
var target=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(at);h.getLevel().addFreshEntity(target);return target;
|
||||
}
|
||||
private static Zombie enemy(GameTestHelper h,Vec3 at) {
|
||||
var target=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(at);h.getLevel().addFreshEntity(target);return target;
|
||||
}
|
||||
@GameTest public void eggProfileSpawnAndAdultRule(GameTestHelper h) {
|
||||
var egg=new ItemStack(Infected.BOOMER_EGG);var mob=Infected.BOOMER.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setBaby(true);
|
||||
h.assertTrue(SpawnEggItem.getType(egg)==Infected.BOOMER&&SpawnEggItem.byId(Infected.BOOMER).orElseThrow().value()==Infected.BOOMER_EGG,"Native egg identifies the new Boomer in both directions");
|
||||
h.assertTrue(!mob.isBaby()&&mob.getMaxHealth()==10&&mob.getAttributeValue(Attributes.MOVEMENT_SPEED)==.20,"Boomer stays adult with its slow role attributes");
|
||||
var profile=FamiliarCatalog.bundled().forEgg(egg);
|
||||
h.assertTrue(profile.id().equals("sanctuary:boomer")&&profile.health()==14&&profile.signature().effect().equals("bile"),"Bundled familiar supplies its own bile profile");
|
||||
var attribute=net.minecraft.world.attribute.EnvironmentAttributes.NATURAL_MOB_SPAWNS;
|
||||
for(var b:h.getLevel().getChunkSource().getGenerator().getBiomeSource().possibleBiomes()) {
|
||||
var entries=b.value().getAttributes().applyModifier(attribute,attribute.defaultValue()).getMobsToSpawn(MobCategory.MONSTER).unwrap();
|
||||
if(entries.stream().noneMatch(e->e.value().type()==EntityTypes.ZOMBIE))continue;
|
||||
var found=entries.stream().filter(e->e.value().type()==Infected.BOOMER).toList();
|
||||
h.assertTrue(found.size()==1&&found.getFirst().weight()==4&&found.getFirst().value().count().minInclusive()==1&&found.getFirst().value().count().maxInclusive()==1,"Boomer receives one rare singleton spawn entry");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=60) public void nativeWindupReleasesBileAndPaysCooldown(GameTestHelper h) {
|
||||
var at=arena(h,1);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));source.setTarget(target);source.setNoAi(false);
|
||||
h.runAfterDelay(10,()->h.assertTrue(source.windingUp()&&!target.hasEffect(MobEffects.GLOWING),"Visible preparation precedes coating"));
|
||||
h.runAfterDelay(27,()->{
|
||||
h.assertTrue(target.hasEffect(MobEffects.GLOWING)&&target.hasEffect(MobEffects.WEAKNESS)&&target.getHealth()==10,"The real AI vomits bile without direct acid damage");
|
||||
h.assertTrue(source.bileCooldown()>0&&!source.windingUp(),"The completed cone enters cooldown");source.discard();target.discard();h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest(maxTicks=60) public void sidestepEvadesTheLockedCone(GameTestHelper h) {
|
||||
var at=arena(h,2);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));source.setTarget(target);source.setNoAi(false);
|
||||
h.runAfterDelay(10,()->target.setPos(Vec3.atBottomCenterOf(at.offset(4,0,3))));
|
||||
h.runAfterDelay(27,()->{h.assertTrue(!target.hasEffect(MobEffects.GLOWING)&&source.bileCooldown()>0,"A side step avoids the original cone direction");source.discard();target.discard();h.succeed();});
|
||||
}
|
||||
@GameTest(maxTicks=45) public void interruptedWindupReloadKeepsOnlyCooldown(GameTestHelper h) {
|
||||
var at=arena(h,3);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));source.setTarget(target);source.setNoAi(false);
|
||||
h.runAfterDelay(10,()->{
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());source.save(output);
|
||||
var tag=output.buildResult();tag.putBoolean("IsBaby",true);
|
||||
var loaded=EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),tag),h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof BoomerEntity b&&!b.isBaby()&&!b.windingUp()&&b.bileCooldown()==120,"A saved preparation reloads as an adult with cooldown, without a replayed cone");
|
||||
for(int x=-3;x<=3;x++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,2),Blocks.STONE.defaultBlockState());
|
||||
});
|
||||
h.runAfterDelay(30,()->{h.assertTrue(!source.windingUp()&&!target.hasEffect(MobEffects.GLOWING),"A wall interrupts preparation before release");source.discard();target.discard();h.succeed();});
|
||||
}
|
||||
@GameTest public void lethalNativeDamageSplashesExactlyOnceWithoutBreakingTerrain(GameTestHelper h) {
|
||||
var at=arena(h,4);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,2)));
|
||||
source.hurtServer(h.getLevel(),source.damageSources().generic(),100);
|
||||
h.assertTrue(!source.isAlive()&&target.getHealth()==8&&target.hasEffect(MobEffects.GLOWING)&&target.hasEffect(MobEffects.WEAKNESS),"Native lethal damage produces one two-damage bile splash");
|
||||
target.setInvulnerableTime(0);source.die(source.damageSources().generic());
|
||||
h.assertTrue(target.getHealth()==8,"Repeated native death notification cannot splash twice");
|
||||
h.assertTrue(h.getLevel().getBlockState(at.below()).is(Blocks.STONE)&&h.getLevel().getBlockState(at).isAir(),"Death never explodes terrain or lights fire");
|
||||
source.discard();target.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void administrativeRemovalDoesNotBurst(GameTestHelper h) {
|
||||
var at=arena(h,5);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,2)));source.discard();
|
||||
h.assertTrue(target.getHealth()==10&&!target.hasEffect(MobEffects.GLOWING),"Discarding or unloading a living Boomer is not a lethal explosion");target.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void wallsHeightBackAndInfectedFamilyBlockCoating(GameTestHelper h) {
|
||||
var at=arena(h,6);var source=boomer(h,at);var front=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,3)));
|
||||
var back=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,-3)));var hidden=cow(h,Vec3.atBottomCenterOf(at.offset(2,0,3)));
|
||||
var upstairs=cow(h,Vec3.atBottomCenterOf(at.offset(0,2,2)));var family=Infected.CHARGER.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
family.setNoAi(true);family.setPos(Vec3.atBottomCenterOf(at.offset(0,0,2)));h.getLevel().addFreshEntity(family);
|
||||
for(int z=1;z<=4;z++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(at.offset(1,y,z),Blocks.STONE.defaultBlockState());
|
||||
h.assertTrue(BoomerBile.wild(source,new Vec3(0,0,1),false)==1&&front.hasEffect(MobEffects.GLOWING),"Only the visible creature inside the forward cone is coated");
|
||||
h.assertTrue(!back.hasEffect(MobEffects.GLOWING)&&!hidden.hasEffect(MobEffects.GLOWING)&&!upstairs.hasEffect(MobEffects.GLOWING)&&!family.hasEffect(MobEffects.GLOWING),"Bile cannot cross walls/floors, reach behind the cone or coat infected relatives");
|
||||
for(var entity:java.util.List.of(source,front,back,hidden,upstairs,family))entity.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void attractionIsBoundedToSixAlreadyPresentZombies(GameTestHelper h) {
|
||||
var at=arena(h,7);var source=boomer(h,at);var victim=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));var zombies=new java.util.ArrayList<Zombie>();
|
||||
for(int i=0;i<8;i++) { var zombie=new StillZombie(h.getLevel());zombie.setPos(Vec3.atBottomCenterOf(at.offset(i%3-1,0,7+i/3)));h.getLevel().addFreshEntity(zombie);zombies.add(zombie); }
|
||||
int before=h.getLevel().getEntitiesOfClass(Zombie.class,source.getBoundingBox().inflate(13)).size();BoomerBile.wild(source,new Vec3(0,0,1),false);
|
||||
h.assertTrue(zombies.stream().filter(z->z.getTarget()==victim).count()==6,"Bile recruits at most six existing zombies");
|
||||
h.assertTrue(h.getLevel().getEntitiesOfClass(Zombie.class,source.getBoundingBox().inflate(13)).size()==before,"The horde effect never spawns extra entities");
|
||||
zombies.forEach(Entity::discard);source.discard();victim.discard();h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=160) public void recruitedZombieReallyPursuesAndThenRestoresPreviousTarget(GameTestHelper h) {
|
||||
var at=arena(h,8);var source=boomer(h,at);var victim=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));victim.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);victim.setHealth(100);
|
||||
var previous=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,14)));var zombie=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
zombie.setPos(Vec3.atBottomCenterOf(at.offset(0,0,10)));zombie.setOnGround(true);h.getLevel().addFreshEntity(zombie);zombie.setTarget(previous);
|
||||
BoomerBile.wild(source,new Vec3(0,0,1),false);
|
||||
h.assertTrue(zombie.getTarget()==victim,"Coating temporarily diverts the native zombie");
|
||||
h.runAfterDelay(70,()->h.assertTrue(zombie.getZ()<at.getZ()+9&&victim.getHealth()<100,"The real zombie navigates toward and attacks the marked creature"));
|
||||
h.runAfterDelay(140,()->{
|
||||
h.assertTrue(zombie.getTarget()==previous&&!victim.hasEffect(MobEffects.GLOWING),"Diversion expires and restores the original valid target");
|
||||
source.discard();victim.discard();previous.discard();zombie.discard();h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest public void familiarGDealsBileDamageAndProtectsOwnerAndPassiveAnimals(GameTestHelper h) {
|
||||
var p=pet(h,9);var pet=FamiliarService.active(p);var front=enemy(h,pet.position().add(0,0,2));var back=enemy(h,pet.position().add(0,0,-2));var animal=cow(h,pet.position().add(1,0,0));
|
||||
float health=p.getHealth();FamiliarBattle.engage(p,front,false);
|
||||
h.assertTrue(FamiliarBattle.activate(p).equals("activated"),"Shared manual entry point accepts the Boomer's signature");
|
||||
var cast=FamiliarBattle.state(p).cast;for(int i=0;i<20;i++)FamiliarTechniques.advance(pet);
|
||||
h.assertTrue(front.getHealth()<20&&back.getHealth()<20&&front.hasEffect(MobEffects.WEAKNESS)&&front.hasEffect(MobEffects.GLOWING)&&cast.hit.size()==2,"G splashes enemies around the familiar using shared damage and effects");
|
||||
h.assertTrue(pet.isAlive()&&pet.getHealth()==14&&p.getHealth()==health&&animal.getHealth()==10&&!animal.hasEffect(MobEffects.GLOWING),"The familiar never sacrifices itself or coats its owner/passive animals");
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&CompanionService.record(p).readyAt()==FamiliarBattle.bond(p).readyAt(),"Released G commits the durable shared cooldown");
|
||||
front.discard();back.discard();animal.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarConeHasNoBackHitAndNeverRedirectsHostiles(GameTestHelper h) {
|
||||
var p=pet(h,10);var pet=FamiliarService.active(p);var front=enemy(h,pet.position().add(0,0,2));var back=enemy(h,pet.position().add(0,0,-2));
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).initiative(),true,front,front.position());FamiliarActions.execute(cast);
|
||||
h.assertTrue(front.getHealth()<20&&front.hasEffect(MobEffects.WEAKNESS)&&back.getHealth()==20&&!back.hasEffect(MobEffects.GLOWING),"Automatic bile affects only the forward cone");
|
||||
h.assertTrue(front.getTarget()==null&&back.getTarget()==null,"Familiar bile does not take control of monsters against third parties");front.discard();back.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void everyMiniatureSizeRetainsAdultGeometryAndBond(GameTestHelper h) {
|
||||
var p=pet(h,11);var id=FamiliarBattle.bond(p).id();
|
||||
for(int value:new int[]{120,500,1000,1500,3000,6000}) {
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount+=10;FamiliarService.tick(p);var pet=FamiliarService.active(p);
|
||||
h.assertTrue(pet!=null&&!pet.babyAppearance()&&FamiliarSize.value(pet.egg())==value&&Math.abs(pet.visualHeight()-.85*value/1000)<.001&&FamiliarBattle.bond(p).id().equals(id),"Every supported size stays an adult miniature with the same identity: "+value);
|
||||
}
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void remoteProjectionDoesNotLoadChunks(GameTestHelper h) {
|
||||
var remote=h.absolutePos(new BlockPos(10000,200,10000));var source=Infected.BOOMER.create(h.getLevel(),EntitySpawnReason.COMMAND);source.setPos(Vec3.atCenterOf(remote));
|
||||
h.assertTrue(!h.getLevel().hasChunkAt(remote)&&BoomerBile.wild(source,new Vec3(0,0,1),false)==0&&!h.getLevel().hasChunkAt(remote),"A remote projection neither affects unloaded mobs nor requests their chunks");h.succeed();
|
||||
}
|
||||
@GameTest public void savingOnTheReleaseTickAlreadyPreservesCooldown(GameTestHelper h) {
|
||||
var at=arena(h,0);var source=boomer(h,at);var target=cow(h,Vec3.atBottomCenterOf(at.offset(0,0,4)));source.setTarget(target);source.setNoAi(false);
|
||||
for(int i=0;i<40&&!target.hasEffect(MobEffects.GLOWING);i++)h.getLevel().tickNonPassenger(source);
|
||||
h.assertTrue(target.hasEffect(MobEffects.GLOWING)&&!source.windingUp()&&source.bileCooldown()==120,"Cooldown is committed on release, before the goal's next stop callback");
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());source.save(output);
|
||||
var loaded=EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()),h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof BoomerEntity b&&b.bileCooldown()==120&&!b.windingUp(),"An immediate save/reload cannot reset the released vomit's cooldown");
|
||||
source.discard();target.discard();h.succeed();
|
||||
}
|
||||
private static ServerPlayer pet(GameTestHelper h,int slot) {
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIE);p.setPos(Vec3.atBottomCenterOf(arena(h,slot)));var egg=new ItemStack(Infected.BOOMER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount=200;FamiliarService.tick(p);var pet=FamiliarService.active(p);pet.setPos(p.position());pet.setOnGround(true);pet.setNoAi(true);return p;
|
||||
}
|
||||
private static void cleanup(ServerPlayer p) { AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);CompanionService.clear(p); }
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.tags.EntityTypeTags;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Real native movement, contact, wall collision and the familiar's straight-charge path. */
|
||||
public final class Charger282GameTests {
|
||||
private static final class StillCow extends net.minecraft.world.entity.animal.cow.Cow {
|
||||
StillCow(net.minecraft.world.level.Level level){super(EntityTypes.COW,level);}
|
||||
@Override protected void registerGoals() { }
|
||||
}
|
||||
private record Wild(ChargerEntity mob,net.minecraft.world.entity.animal.cow.Cow target,BlockPos origin) {
|
||||
void close(){mob.discard();target.discard();}
|
||||
}
|
||||
private static BlockPos arena(GameTestHelper h,int slot) {
|
||||
var at=h.absolutePos(new BlockPos(0,70+slot*8,0));
|
||||
// Keep the four-block sidestep on the arena floor instead of testing fall damage.
|
||||
for(int x=-5;x<=5;x++)for(int z=-2;z<=34;z++) {
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,-1,z),Blocks.STONE.defaultBlockState());
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,4,z),Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
return at;
|
||||
}
|
||||
private static Wild wild(GameTestHelper h,int slot) {
|
||||
var at=arena(h,slot);var mob=Infected.CHARGER.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
mob.setPos(Vec3.atBottomCenterOf(at));mob.setOnGround(true);
|
||||
var target=new StillCow(h.getLevel());
|
||||
target.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);
|
||||
target.setPos(Vec3.atBottomCenterOf(at.offset(0,0,7)));
|
||||
h.getLevel().addFreshEntity(target);h.getLevel().addFreshEntity(mob);mob.setTarget(target);
|
||||
return new Wild(mob,target,at);
|
||||
}
|
||||
@GameTest public void eggProfileTagsAndAdultRuleAreRegistered(GameTestHelper h) {
|
||||
var egg=new ItemStack(Infected.CHARGER_EGG);var mob=Infected.CHARGER.create(h.getLevel(),EntitySpawnReason.MOB_SUMMONED);
|
||||
mob.setBaby(true);
|
||||
h.assertTrue(SpawnEggItem.getType(egg)==Infected.CHARGER&&SpawnEggItem.byId(Infected.CHARGER).orElseThrow().value()==Infected.CHARGER_EGG,"Native charger egg mapping is bidirectional");
|
||||
h.assertTrue(!mob.isBaby()&&mob.getMaxHealth()==10&&mob.getAttributeValue(Attributes.MOVEMENT_SPEED)==.27,"Charger is adult-only without the baby speed bonus");
|
||||
h.assertTrue(Infected.CHARGER.builtInRegistryHolder().is(EntityTypeTags.UNDEAD)&&Infected.CHARGER.builtInRegistryHolder().is(EntityTypeTags.BURN_IN_DAYLIGHT),"Charger remains an undead daylight-sensitive zombie");
|
||||
h.assertTrue(FamiliarCatalog.bundled().forEgg(egg).id().equals("sanctuary:charger"),"Charger has a dedicated bundled profile");
|
||||
var attribute=net.minecraft.world.attribute.EnvironmentAttributes.NATURAL_MOB_SPAWNS;
|
||||
for(var b:h.getLevel().getChunkSource().getGenerator().getBiomeSource().possibleBiomes()) {
|
||||
var entries=b.value().getAttributes().applyModifier(attribute,attribute.defaultValue()).getMobsToSpawn(MobCategory.MONSTER).unwrap();
|
||||
if(entries.stream().noneMatch(e->e.value().type()==EntityTypes.ZOMBIE))continue;
|
||||
var found=entries.stream().filter(e->e.value().type()==Infected.CHARGER).toList();
|
||||
h.assertTrue(found.size()==1&&found.getFirst().weight()==4&&found.getFirst().value().count().minInclusive()==1&&found.getFirst().value().count().maxInclusive()==1,"Zombie habitats receive one rare singleton charger entry");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=150) public void windupImpactRecoveryAndCooldownUseRealPhysics(GameTestHelper h) {
|
||||
var w=wild(h,1);var start=w.mob.position();
|
||||
h.runAfterDelay(10,()->h.assertTrue(w.mob.phase()==ChargerEntity.Phase.WINDUP&&w.mob.position().subtract(start).horizontalDistance()<.1,"The charger holds its position while visibly preparing"));
|
||||
h.runAfterDelay(45,()->{
|
||||
h.assertTrue(w.target.getHealth()==96&&w.target.getZ()>w.origin.getZ()+7.5,"One real charge impact deals four damage and pushes the target: health="+w.target.getHealth()+" z="+w.target.getZ()+" origin="+w.origin+" phase="+w.mob.phase());
|
||||
h.assertTrue(w.mob.phase()==ChargerEntity.Phase.RECOVERING,"Impact enters recovery");
|
||||
h.assertTrue(!w.mob.doHurtTarget(h.getLevel(),w.target),"Native melee cannot double-hit during recovery");
|
||||
});
|
||||
h.runAfterDelay(110,()->{h.assertTrue(w.mob.phase()==ChargerEntity.Phase.IDLE&&w.mob.chargeCooldown()>0,"Recovery ends before another charge is permitted");w.close();h.succeed();});
|
||||
}
|
||||
@GameTest(maxTicks=120) public void movingAsideDodgesInsteadOfBeingHomedOn(GameTestHelper h) {
|
||||
var w=wild(h,2);var started=new java.util.concurrent.atomic.AtomicBoolean();double x=w.mob.getX();
|
||||
h.onEachTick(()->{if(w.mob.phase()==ChargerEntity.Phase.CHARGING&&started.compareAndSet(false,true)){
|
||||
w.target.setPos(w.target.position().add(4,0,0));
|
||||
h.runAfterDelay(30,()->{
|
||||
h.assertTrue(Math.abs(w.mob.getX()-x)<.01&&w.mob.getZ()>w.origin.getZ()+4,"A released charge keeps its original straight direction");
|
||||
h.assertTrue(w.target.getHealth()==100&&w.mob.phase()==ChargerEntity.Phase.RECOVERING,"A sidestep avoids contact and a miss still requires recovery: health="+w.target.getHealth()+" target="+w.target.position()+" phase="+w.mob.phase());
|
||||
w.close();h.succeed();
|
||||
});
|
||||
}});
|
||||
}
|
||||
@GameTest(maxTicks=120) public void newWallStopsTheBodyWithoutDamageThroughIt(GameTestHelper h) {
|
||||
var w=wild(h,3);var started=new java.util.concurrent.atomic.AtomicBoolean();
|
||||
h.onEachTick(()->{if(w.mob.phase()==ChargerEntity.Phase.CHARGING&&started.compareAndSet(false,true)){
|
||||
for(int x=-3;x<=3;x++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(w.origin.offset(x,y,4),Blocks.STONE.defaultBlockState());
|
||||
h.runAfterDelay(25,()->{
|
||||
h.assertTrue(w.mob.getZ()<=w.origin.getZ()+3.71&&w.target.getHealth()==100,"Native collision stops the actual body and swept contact cannot cross the wall");
|
||||
h.assertTrue(w.mob.phase()==ChargerEntity.Phase.RECOVERING&&h.getLevel().getBlockState(w.origin.offset(0,1,4)).is(Blocks.STONE),"Wall collision causes recovery without breaking blocks");
|
||||
w.close();h.succeed();
|
||||
});
|
||||
}});
|
||||
}
|
||||
@GameTest(maxTicks=100) public void reloadingAnInterruptedChargePreservesOnlyCooldown(GameTestHelper h) {
|
||||
var w=wild(h,4);w.mob.setHealth(7);
|
||||
h.runAfterDelay(10,()->{
|
||||
w.mob.setDeltaMovement(1.18,0,0);
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());w.mob.save(output);
|
||||
var saved=output.buildResult();saved.putBoolean("IsBaby",true);
|
||||
var loaded=EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),saved),h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof ChargerEntity c&&c.getUUID().equals(w.mob.getUUID())&&c.getHealth()==7&&!c.isBaby()
|
||||
&&c.phase()==ChargerEntity.Phase.IDLE&&c.chargeCooldown()==100&&c.chargeDirection().equals(Vec3.ZERO)&&c.getDeltaMovement().horizontalDistance()==0,"Reload retains UUID and health but cannot resume or reset a partly prepared rush");
|
||||
w.close();h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest public void familiarActuallyRushesAndCommitsSharedCooldown(GameTestHelper h) {
|
||||
var p=pet(h,6);var pet=FamiliarService.active(p);var enemy=enemy(p);var profile=FamiliarBattle.profile(p);
|
||||
h.assertTrue(!pet.babyAppearance()&&profile.id().equals("sanctuary:charger")&&pet.getMaxHealth()==16,"Equipped charger is a miniature adult using its own profile");
|
||||
FamiliarBattle.engage(p,enemy,false);h.assertTrue(FamiliarBattle.activate(p).equals("activated"),"The normal manual technique entry point starts the rush");
|
||||
var cast=FamiliarBattle.state(p).cast;run(p,80);
|
||||
h.assertTrue(enemy.getHealth()<20&&enemy.getHealth()>=16&&cast.hit.size()==1,"The real moved body hits once through shared damage and armor rules");
|
||||
h.assertTrue(enemy.getDeltaMovement().z>0&&FamiliarBattle.state(p).cast==null,"Impact pushes the enemy and recovery completes");
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&CompanionService.record(p).readyAt()==FamiliarBattle.bond(p).readyAt(),"The durable shared cooldown is committed at release");
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarMissKeepsStraightTrajectoryAndPaysCooldown(GameTestHelper h) {
|
||||
var p=pet(h,7);var pet=FamiliarService.active(p);var enemy=enemy(p);double x=pet.getX();
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());FamiliarBattle.state(p).cast=cast;
|
||||
run(p,22);enemy.setPos(enemy.position().add(4,0,0));run(p,70);
|
||||
h.assertTrue(Math.abs(pet.getX()-x)<.001&&pet.getZ()>p.getZ()+4&&enemy.getHealth()==20,"The familiar does not home in on a sidestepping target");
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&FamiliarBattle.state(p).cast==null,"A missed released charge still consumes cooldown and recovery");
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarStopsBeforeWallsAndCannotChargeItsOwner(GameTestHelper h) {
|
||||
var p=pet(h,8);var pet=FamiliarService.active(p);var enemy=enemy(p);double wall=pet.getZ()+2;
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());FamiliarBattle.state(p).cast=cast;run(p,22);
|
||||
var pos=BlockPos.containing(pet.getX(),pet.getY(),wall);
|
||||
for(int x=-2;x<=2;x++)for(int y=0;y<3;y++)p.level().setBlockAndUpdate(pos.offset(x,y,0),Blocks.STONE.defaultBlockState());
|
||||
run(p,70);h.assertTrue(enemy.getHealth()==20&&pet.getZ()<wall&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"An obstacle stops the familiar without a through-wall hit or a cooldown refund");
|
||||
float health=p.getHealth();var denied=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,p,p.position());FamiliarBattle.state(p).cast=denied;run(p,30);
|
||||
h.assertTrue(p.getHealth()==health&&denied.hit.isEmpty(),"Even a server-created cast cannot bypass the owner protection rules");
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void ownerTeleportCannotBecomeASweptRemoteHit(GameTestHelper h) {
|
||||
var p=pet(h,9);var pet=FamiliarService.active(p);var enemy=enemy(p);
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());FamiliarBattle.state(p).cast=cast;
|
||||
run(p,22);pet.teleportTo(pet.getX(),pet.getY(),pet.getZ()+10);run(p,40);
|
||||
h.assertTrue(enemy.getHealth()==20&&cast.hit.isEmpty()&&FamiliarBattle.state(p).cast==null,"A discontinuous relocation across the target cancels contact rather than hitting from a distance");
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"An interrupted released charge retains its cooldown");
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void mountedWindupBlocksSteeringAndThenRestoresIt(GameTestHelper h) {
|
||||
var p=pet(h,10);var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();
|
||||
size.putInt("schema",1);size.putInt("per_mille",2500);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
var floor=p.blockPosition();
|
||||
for(int x=-3;x<=3;x++)for(int z=-2;z<=34;z++) {
|
||||
p.level().setBlockAndUpdate(floor.offset(x,4,z),Blocks.AIR.defaultBlockState());
|
||||
p.level().setBlockAndUpdate(floor.offset(x,8,z),Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount+=10;FamiliarService.tick(p);var pet=FamiliarService.active(p);pet.setPos(Vec3.atBottomCenterOf(floor));pet.setOnGround(true);
|
||||
h.assertTrue(pet.mount(p),"The existing colossal familiar can be mounted");
|
||||
var enemy=enemy(p);FamiliarBattle.state(p).cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());
|
||||
p.setYRot(90);p.setLastClientInput(new net.minecraft.world.entity.player.Input(true,false,false,true,true,false,false));
|
||||
pet.setDeltaMovement(Vec3.ZERO);var start=pet.position();pet.travel(Vec3.ZERO);
|
||||
h.assertTrue(pet.position().subtract(start).horizontalDistance()<.001&&pet.getDeltaMovement().horizontalDistance()<.001,"A rider cannot steer or jump out of the committed charge windup");
|
||||
FamiliarBattle.cancel(p);pet.setOnGround(true);pet.travel(Vec3.ZERO);
|
||||
h.assertTrue(pet.getDeltaMovement().horizontalDistance()>0,"Normal rider movement resumes when the cast ends");
|
||||
p.stopRiding();enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void unloadedCorridorIsRejectedWithoutLoadingIt(GameTestHelper h) {
|
||||
var at=h.absolutePos(new BlockPos(10000,90,10000));
|
||||
h.assertTrue(!h.getLevel().hasChunkAt(at),"The remote corridor starts unloaded");
|
||||
h.assertTrue(!ChargeContact.loaded(h.getLevel(),new net.minecraft.world.phys.AABB(at).inflate(.3)),"A full body corridor must already be loaded");
|
||||
h.assertTrue(!h.getLevel().hasChunkAt(at),"The charge probe does not request or generate the remote chunk");h.succeed();
|
||||
}
|
||||
private static ServerPlayer pet(GameTestHelper h,int slot) {
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIE);var at=arena(h,slot);
|
||||
p.setPos(Vec3.atBottomCenterOf(at));var egg=new ItemStack(Infected.CHARGER_EGG);
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount=200;FamiliarService.tick(p);
|
||||
var pet=FamiliarService.active(p);pet.setPos(p.position());pet.setOnGround(true);return p;
|
||||
}
|
||||
private static net.minecraft.world.entity.monster.zombie.Zombie enemy(ServerPlayer p) {
|
||||
var enemy=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);enemy.setNoAi(true);enemy.setPos(p.position().add(0,0,5));p.level().addFreshEntity(enemy);return enemy;
|
||||
}
|
||||
private static void run(ServerPlayer p,int ticks) {
|
||||
var pet=FamiliarService.active(p);
|
||||
for(int i=0;i<ticks&&FamiliarBattle.state(p).cast!=null;i++) {
|
||||
pet.setOnGround(true);FamiliarTechniques.advance(pet);pet.move(MoverType.SELF,pet.getDeltaMovement().multiply(1,0,1));pet.setDeltaMovement(Vec3.ZERO);
|
||||
}
|
||||
}
|
||||
private static void cleanup(ServerPlayer p) { AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);CompanionService.clear(p); }
|
||||
}
|
||||
+3
-2
@@ -56,10 +56,11 @@ public final class ChunkClaims272GameTests {
|
||||
}
|
||||
@GameTest public void nativeDurationUsesActualChunkAndKeepsInteractionsFree(GameTestHelper h)throws Exception{
|
||||
var owner=admitted(h,340,340);var other=admitted(h,340,340);var s=ChunkPropertyService.store(owner.level().getServer());
|
||||
h.assertTrue(act(owner,"claim",340,340).equals("claimed"),"Reserve chunk");
|
||||
var dispatcher=owner.level().getServer().getCommands().getDispatcher();var source=owner.createCommandSourceStack();
|
||||
h.assertTrue(dispatcher.execute("sanctuary property claim",source)==1&&s.get(new ChunkClaims.Key(340,340)).owner().equals(owner.getUUID()),"English command reserves the current chunk");
|
||||
var pos=new BlockPos(340*16+15,80,340*16+2);var state=Blocks.STONE.defaultBlockState();owner.level().setBlockAndUpdate(pos,state);
|
||||
float ownBase=state.getDestroyProgress(owner,owner.level(),pos),otherBase=state.getDestroyProgress(other,other.level(),pos);
|
||||
owner.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),64));h.assertTrue(act(owner,"protect",340,340).equals("protected"),"Activate protection");
|
||||
owner.getInventory().setItem(9,new ItemStack(Gems.SAPPHIRE.gem(),64));h.assertTrue(dispatcher.execute("sanctuary property protect",source)==1&&ChunkPropertyService.sapphires(owner)==56,"English command activates protection for eight sapphires");
|
||||
h.assertTrue(state.getDestroyProgress(owner,owner.level(),pos)==ownBase*2&&state.getDestroyProgress(other,other.level(),pos)==otherBase*.5f,"Real native mining duration hook multiplies once");
|
||||
h.assertTrue(state.getDestroyProgress(other,other.level(),pos.east())==otherBase,"Adjacent chunk has no slowdown");
|
||||
h.assertTrue(ChunkPropertyService.multiplier(owner,owner.level(),pos.above(100))==2&&ChunkPropertyService.multiplier(other,other.level(),pos.below(100))==.5f,"Chunk column covers all build heights");
|
||||
|
||||
+3
-1
@@ -24,7 +24,9 @@ public final class Companion019GameTests {
|
||||
@GameTest public void catalogMatchesEveryNativeEgg(GameTestHelper h){
|
||||
var found=new HashSet<CompanionType>();
|
||||
for(var item:BuiltInRegistries.ITEM)if(item instanceof SpawnEggItem){
|
||||
var type=CompanionType.find(BuiltInRegistries.ENTITY_TYPE.getKey(SpawnEggItem.getType(new ItemStack(item))).toString());
|
||||
var id=BuiltInRegistries.ENTITY_TYPE.getKey(SpawnEggItem.getType(new ItemStack(item)));
|
||||
if(!id.getNamespace().equals("minecraft"))continue;
|
||||
var type=CompanionType.find(id.toString());
|
||||
h.assertTrue(type!=null,"Each native egg has a configured power definition");found.add(type);
|
||||
}
|
||||
h.assertTrue(found.size()==88&&found.size()==CompanionType.values().length,"Exactly 88 native familiar species");h.succeed();
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.whale.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
public final class FlyingWhale290GameTests {
|
||||
@GameTest public void cloudKeepsHistoricalAttributesAndGhostRules(GameTestHelper h) {
|
||||
var whale = FlyingWhales.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
h.assertTrue(whale.getMaxHealth() == 500 && whale.getAttributeValue(Attributes.FOLLOW_RANGE) == 520,
|
||||
"Historical internal health and player range are preserved");
|
||||
h.assertTrue(whale.getBbWidth() == 36 && whale.getBbHeight() == 11 && whale.isNoGravity()
|
||||
&& whale.isInvulnerable() && whale.noPhysics && whale.fireImmune(), "Giant flying cloud is invulnerable and intangible");
|
||||
h.assertTrue(!whale.blocksBuilding && !whale.isAttackable() && !whale.isPickable()
|
||||
&& !whale.isPushable() && !whale.canBeHitByProjectile() && !whale.isPushedByFluid(),
|
||||
"Whale cannot obstruct, attack or intercept projectiles");
|
||||
float health = whale.getHealth();
|
||||
h.assertTrue(!whale.hurtServer(h.getLevel(), h.getLevel().damageSources().genericKill(), 1000)
|
||||
&& whale.getHealth() == health, "Even direct server damage is refused");
|
||||
whale.push(2, 3, 4);
|
||||
h.assertTrue(whale.getDeltaMovement().equals(Vec3.ZERO) && !whale.removeWhenFarAway(1_000_000),
|
||||
"No knockback or vanilla distance despawn");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void nativeCollisionDoesNotBreakTerrain(GameTestHelper h) {
|
||||
var whale = FlyingWhales.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
var pos = h.absolutePos(new BlockPos(2, 2, 2));
|
||||
h.getLevel().setBlockAndUpdate(pos, Blocks.STONE.defaultBlockState());
|
||||
whale.setPos(Vec3.atCenterOf(pos).add(-2, 0, 0));
|
||||
var start = whale.position();
|
||||
whale.move(MoverType.SELF, new Vec3(4, 0, 0));
|
||||
h.assertTrue(whale.position().distanceTo(start.add(4, 0, 0)) < .00001
|
||||
&& h.getLevel().getBlockState(pos).is(Blocks.STONE), "Native movement passes through stone without editing it");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void onlyNamedWhalesPersistAndNativeReloadRestoresGhostRules(GameTestHelper h) {
|
||||
var whale = FlyingWhales.TYPE.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
h.assertTrue(!whale.shouldBeSaved(), "Unnamed visitors are transient, as in 26.2");
|
||||
whale.setCustomName(Component.literal("Nuage")); whale.seedOrbit(2.25);
|
||||
var output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, h.getLevel().registryAccess());
|
||||
h.assertTrue(whale.shouldBeSaved() && whale.save(output), "Named whale can be saved natively");
|
||||
var saved = output.buildResult();
|
||||
h.assertTrue(saved.getDoubleOr("OrbitAngle", 0) == 2.25, "Historical orbit angle survives native save");
|
||||
saved.putBoolean("NoGravity", false); saved.putBoolean("Invulnerable", false);
|
||||
var input = TagValueInput.create(ProblemReporter.DISCARDING, h.getLevel().registryAccess(), saved);
|
||||
var loaded = EntityType.loadEntityRecursive(input, h.getLevel(), EntitySpawnReason.LOAD, e -> e);
|
||||
h.assertTrue(loaded instanceof FlyingWhaleEntity ghost && ghost.getUUID().equals(whale.getUUID())
|
||||
&& ghost.getName().getString().equals("Nuage") && ghost.isNoGravity() && ghost.isInvulnerable()
|
||||
&& ghost.noPhysics, "Identity, name and ghost physics survive a native save/reload");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void scheduleHasThreeWindowsAndNoCrossServerStaticCounter(GameTestHelper h) {
|
||||
var schedule = new FlyingWhaleSpawner.Schedule();
|
||||
for (int i = 0; i < 399; i++) h.assertTrue(!schedule.due(), "No spawn check before 400 ticks");
|
||||
h.assertTrue(schedule.due() && !schedule.due(), "Check at 400 ticks then reset");
|
||||
h.assertTrue(schedule.window(0) == 0 && schedule.available(0), "First window starts available");
|
||||
schedule.spawned(0);
|
||||
h.assertTrue(!schedule.available(0) && schedule.window(7999) == 0 && !schedule.available(0), "One success per daily window");
|
||||
h.assertTrue(schedule.window(8000) == 1 && schedule.available(1), "Second window starts at 8000");
|
||||
schedule.spawned(1);
|
||||
h.assertTrue(schedule.window(16000) == 2 && schedule.available(2), "Third window starts at 16000");
|
||||
schedule.spawned(2);
|
||||
h.assertTrue(schedule.window(24000) == 0 && schedule.available(0), "New day resets windows");
|
||||
h.assertTrue(new FlyingWhaleSpawner.Schedule().available(0), "A new server has an independent schedule");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void naturalPlacementKeepsOverworldAndHeightBounds(GameTestHelper h) {
|
||||
var level = h.getLevel(); var random = RandomSource.create(290);
|
||||
h.assertTrue(!FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, level.getMinY() + 18, 0), random), "Lower altitude margin is excluded");
|
||||
h.assertTrue(!FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, level.getMaxY() - 18, 0), random), "Upper altitude margin is excluded");
|
||||
h.assertTrue(FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, level, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, level.getMinY() + 30, 0), random), "Free Overworld altitude is accepted");
|
||||
var nether = level.getServer().getLevel(Level.NETHER);
|
||||
h.assertTrue(!FlyingWhaleEntity.canSpawn(FlyingWhales.TYPE, nether, EntitySpawnReason.NATURAL,
|
||||
new BlockPos(0, 64, 0), random), "Natural visitors stay in the Overworld");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest(maxTicks = 100) public void eggQuotaNaturalSpawnAndFlightUseActualServerEntities(GameTestHelper h) {
|
||||
var level = h.getLevel(); var server = level.getServer(); var p = Progression003GameTests.player(h);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(FlyingWhales.SPAWN_EGG, 3));
|
||||
var at = p.blockPosition().below();
|
||||
level.setBlockAndUpdate(at, Blocks.STONE.defaultBlockState());
|
||||
var context = new UseOnContext(p, InteractionHand.MAIN_HAND,
|
||||
new BlockHitResult(Vec3.atCenterOf(at).add(0, .5, 0), Direction.UP, at, false));
|
||||
h.assertTrue(SpawnEggItem.getType(p.getMainHandItem()) == FlyingWhales.TYPE, "Native egg mapping resolves the whale");
|
||||
FlyingWhales.SPAWN_EGG.useOn(context);
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 1 && p.getMainHandItem().getCount() == 2,
|
||||
"Real survival egg use spawns one whale and consumes one egg");
|
||||
var first = level.getEntities(FlyingWhales.TYPE, Entity::isAlive).getFirst();
|
||||
first.setNoAi(true);
|
||||
// Its 36-block hitbox must not overlap neighbouring tests' cleanup areas.
|
||||
first.setPos(p.position().add(0, 80, 0));
|
||||
var nether = server.getLevel(Level.NETHER); nether.setChunkForced(0, 0, true); nether.getChunk(0, 0);
|
||||
nether.waitForEntities(new net.minecraft.world.level.ChunkPos(0, 0), 0);
|
||||
var other = FlyingWhales.TYPE.create(nether, EntitySpawnReason.COMMAND);
|
||||
// Keep the quota fixture inside its single forced chunk during the delay.
|
||||
other.setNoAi(true); other.setPos(8, 64, 8); nether.addFreshEntity(other);
|
||||
h.runAfterDelay(5, () -> {
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 2,
|
||||
"Both quota fixtures remain loaded before egg use; firstRemoved=" + first.isRemoved()
|
||||
+ ", netherRemoved=" + other.isRemoved());
|
||||
FlyingWhales.SPAWN_EGG.useOn(context);
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 2 && p.getMainHandItem().getCount() == 2,
|
||||
"Two across dimensions block the third egg without consuming it; count=" + FlyingWhales.countLoaded(server)
|
||||
+ ", eggs=" + p.getMainHandItem().getCount() + ", netherRemoved=" + other.isRemoved());
|
||||
var third = FlyingWhales.TYPE.create(level, EntitySpawnReason.COMMAND);
|
||||
third.setPos(p.position()); level.addFreshEntity(third);
|
||||
h.assertTrue(third.isRemoved() && FlyingWhales.countLoaded(server) == 2, "Commands cannot exceed the shared limit");
|
||||
other.discard();
|
||||
nether.setChunkForced(0, 0, false);
|
||||
FlyingWhales.SPAWN_EGG.useOn(context);
|
||||
h.assertTrue(FlyingWhales.countLoaded(server) == 2 && p.getMainHandItem().getCount() == 1,
|
||||
"Removing a whale releases its place immediately");
|
||||
level.getEntities(FlyingWhales.TYPE, Entity::isAlive).forEach(Entity::discard);
|
||||
// Load only development chunks for this deterministic radius test.
|
||||
for (int x = -10; x <= 10; x++) for (int z = -10; z <= 10; z++)
|
||||
level.getChunk(p.blockPosition().getX() / 16 + x, p.blockPosition().getZ() / 16 + z);
|
||||
h.assertTrue(FlyingWhaleSpawner.spawnFor(level, p, RandomSource.create(290)), "Actual natural spawner creates a visitor");
|
||||
var whale = level.getEntities(FlyingWhales.TYPE, Entity::isAlive).getFirst();
|
||||
double radius = whale.position().subtract(p.position()).horizontalDistance();
|
||||
h.assertTrue(radius >= 92 && radius < 138 && whale.getY() >= level.getMinY() + 22
|
||||
&& whale.getY() <= level.getMaxY() - 22, "Native spawn retains historical radius and altitude");
|
||||
whale.setPos(p.position().add(0, 30, 0));
|
||||
var start = whale.position();
|
||||
h.runAfterDelay(30, () -> {
|
||||
h.assertTrue(whale.position().distanceToSqr(start) > .001 && whale.isNoGravity() && whale.noPhysics,
|
||||
"Historical AI actually moves through the air on native server ticks");
|
||||
whale.tickCount = 14400;
|
||||
});
|
||||
h.runAfterDelay(33, () -> {
|
||||
h.assertTrue(whale.isRemoved() && FlyingWhales.countLoaded(server) == 0,
|
||||
"Historical twelve minute lifetime fades and releases quota");
|
||||
h.succeed();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.creeper.HappyCreepers;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.monster.Creeper;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
|
||||
public final class HappyCreeperGameTests {
|
||||
@GameTest public void ordinaryHeadGestureConsumesAndReturnsTheSameFlower(GameTestHelper h) {
|
||||
var p = player(h); var mob = creeper(h); mob.setPos(p.position().add(0, 0, 1));
|
||||
var flower = new ItemStack(Items.POPPY, 3);
|
||||
flower.set(net.minecraft.core.component.DataComponents.CUSTOM_NAME, net.minecraft.network.chat.Component.literal("Rose"));
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, flower); p.setShiftKeyDown(true);
|
||||
h.assertTrue(MobHeadService.equip(p, mob), "Existing head gesture equips the flower");
|
||||
h.assertTrue(flower.getCount() == 2 && HappyCreepers.isHappy(mob), "One real flower is consumed and calms the creeper");
|
||||
h.assertTrue(ItemStack.matches(HeadCosmeticService.appearance(mob), flower.copyWithCount(1)), "Exact flower and components are synchronized");
|
||||
h.assertTrue(mob.requiresCustomPersistence(), "Equipped creeper does not despawn at distance");
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
|
||||
h.assertTrue(MobHeadService.equip(p, mob) && !HappyCreepers.isHappy(mob), "Empty-hand head gesture removes happiness");
|
||||
h.assertTrue(p.getInventory().countItem(Items.POPPY) == 1 && p.getInventory().getNonEquipmentItems().stream()
|
||||
.anyMatch(s -> s.is(Items.POPPY) && ItemStack.matches(s, flower.copyWithCount(1))), "The same named flower returns once");
|
||||
mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void everyTaggedFlowerWorksButOtherHeadObjectsDoNot(GameTestHelper h) {
|
||||
var mob = creeper(h); int flowers = 0;
|
||||
for (var item : BuiltInRegistries.ITEM) {
|
||||
var stack = new ItemStack(item);
|
||||
if (!HappyCreepers.isFlower(stack)) continue;
|
||||
HeadEquipment.set(mob, stack);
|
||||
h.assertTrue(HappyCreepers.isHappy(mob), "Tagged flower is accepted: " + item); flowers++;
|
||||
}
|
||||
h.assertTrue(flowers >= 15, "The exact Minecraft version resolves its flower tag");
|
||||
for (var item : java.util.List.of(Items.FLOWER_POT, Items.OAK_LEAVES, Items.WHEAT_SEEDS, Items.STONE)) {
|
||||
HeadEquipment.set(mob, new ItemStack(item));
|
||||
h.assertTrue(!HappyCreepers.isHappy(mob), "A different head object keeps the ordinary temperament");
|
||||
}
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void flowerImmediatelyCancelsAnAlreadyIgnitedFuse(GameTestHelper h) {
|
||||
var mob = creeper(h); var p = player(h);
|
||||
mob.setTarget(p); mob.ignite();
|
||||
for (int i = 0; i < 20; i++) mob.tick();
|
||||
h.assertTrue(mob.isIgnited() && mob.getSwelling(1) > 0, "Real native fuse is already progressing");
|
||||
HeadEquipment.set(mob, new ItemStack(Items.DANDELION));
|
||||
h.assertTrue(!mob.isIgnited() && mob.getTarget() == null && mob.getSwelling(1) == 0, "Equipment immediately clears target, ignition and accumulated fuse");
|
||||
for (int i = 0; i < 60; i++) { mob.setTarget(p); mob.setSwellDir(1); mob.ignite(); mob.tick(); }
|
||||
h.assertTrue(mob.isAlive() && mob.getTarget() == null && !mob.isIgnited() && mob.getSwellDir() < 0 && mob.getSwelling(1) == 0,
|
||||
"Repeated targeting and ignition cannot make a happy creeper explode");
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.tick();
|
||||
h.assertTrue(mob.isAlive() && !mob.isIgnited() && mob.getSwelling(1) == 0, "Removing the flower cannot resume an old fuse");
|
||||
mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void happyCreeperRejectsRetaliationAndKeepsItsOrdinaryMovement(GameTestHelper h) {
|
||||
var mob = creeper(h); var p = player(h); mob.setNoAi(false);
|
||||
HeadEquipment.set(mob, new ItemStack(Items.CORNFLOWER));
|
||||
mob.hurtServer(h.getLevel(), mob.damageSources().playerAttack(p), 1);
|
||||
for (int i = 0; i < 40; i++) mob.tick();
|
||||
h.assertTrue(mob.getTarget() == null && !mob.isAggressive() && mob.isAlive() && !mob.isNoAi(), "Damage does not restore hostility or disable the native AI");
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.setTarget(p);
|
||||
h.assertTrue(mob.getTarget() == p, "Removing the flower restores normal targeting");
|
||||
mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void flintAndFireChargeCannotIgniteOrSpendItemsWhileHappy(GameTestHelper h) {
|
||||
var mob = creeper(h); var p = player(h);
|
||||
HeadEquipment.set(mob, new ItemStack(Items.BLUE_ORCHID));
|
||||
for (var hand : InteractionHand.values()) for (var item : java.util.List.of(Items.FLINT_AND_STEEL, Items.FIRE_CHARGE)) {
|
||||
var stack = new ItemStack(item, item == Items.FIRE_CHARGE ? 3 : 1);
|
||||
var before = stack.copy(); p.setItemInHand(hand, stack);
|
||||
mob.interact(p, hand, net.minecraft.world.phys.Vec3.ZERO);
|
||||
h.assertTrue(!mob.isIgnited() && ItemStack.matches(stack, before), "Igniter remains intact in either hand");
|
||||
}
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void chargedHappyCreeperKeepsIdentityFlowerAndChargeAfterNativeReload(GameTestHelper h) {
|
||||
var mob = creeper(h);
|
||||
HeadEquipment.set(mob, new ItemStack(Items.PINK_TULIP));
|
||||
mob.thunderHit(h.getLevel(), EntityTypes.LIGHTNING_BOLT.create(h.getLevel(), EntitySpawnReason.COMMAND));
|
||||
h.assertTrue(HappyCreepers.isHappy(mob) && mob.isPowered(), "Lightning charges a happy creeper without changing its temperament");
|
||||
var output = TagValueOutput.createWithContext(ProblemReporter.DISCARDING, h.getLevel().registryAccess());
|
||||
h.assertTrue(mob.save(output), "Vanilla entity saves normally");
|
||||
var loaded = (Creeper) EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,
|
||||
h.getLevel().registryAccess(), output.buildResult()), h.getLevel(), EntitySpawnReason.LOAD, e -> e);
|
||||
h.assertTrue(loaded.getType() == EntityTypes.CREEPER && loaded.getUUID().equals(mob.getUUID()) && loaded.isPowered()
|
||||
&& HappyCreepers.isHappy(loaded) && HeadEquipment.get(loaded).is(Items.PINK_TULIP), "Existing head save preserves the same creeper, flower and independent electric charge");
|
||||
loaded.ignite(); loaded.tick(); h.assertTrue(!loaded.isIgnited() && loaded.isAlive(), "Reloaded flower still prevents explosions");
|
||||
HeadEquipment.set(loaded, ItemStack.EMPTY);
|
||||
h.assertTrue(loaded.isPowered() && !HappyCreepers.isHappy(loaded), "Taking the flower leaves the electric charge intact");
|
||||
HeadEquipment.set(mob, ItemStack.EMPTY); mob.discard(); loaded.discard(); h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void ordinaryAndUnequippedCreepersStillExplodeNormally(GameTestHelper h) {
|
||||
for (boolean previouslyHappy : new boolean[]{false, true}) {
|
||||
var mob = creeper(h);
|
||||
if (previouslyHappy) { HeadEquipment.set(mob, new ItemStack(Items.POPPY)); HeadEquipment.set(mob, ItemStack.EMPTY); }
|
||||
mob.ignite(); h.assertTrue(mob.isIgnited(), "Normal ignition is available without a flower");
|
||||
for (int i = 0; i < 31 && !mob.isRemoved(); i++) mob.tick();
|
||||
h.assertTrue(mob.isRemoved(), "Native fuse and explosion still finish without a flower");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
private static Creeper creeper(GameTestHelper h) {
|
||||
var mob = EntityTypes.CREEPER.create(h.getLevel(), EntitySpawnReason.COMMAND);
|
||||
mob.setNoAi(true); mob.setPos(h.absoluteVec(new net.minecraft.world.phys.Vec3(2, 2, 2))); return mob;
|
||||
}
|
||||
private static ServerPlayer player(GameTestHelper h) {
|
||||
var p = Progression003GameTests.player(h); p.setUUID(java.util.UUID.randomUUID());
|
||||
p.setAttached(ProgressionService.RECORD, ProgressRecord.INITIAL.json()); ProgressEffects.apply(p, ProgressRecord.INITIAL); return p;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.animal.cow.Cow;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Actual native airborne physics and swept body hits, including the familiar's G path. */
|
||||
public final class Hunter286GameTests {
|
||||
private static final java.util.concurrent.atomic.AtomicReference<BlockPos> DENIED=new java.util.concurrent.atomic.AtomicReference<>();
|
||||
static { CompanionWorld.ALLOW.register((player,pos,operation)->!pos.equals(DENIED.get())); }
|
||||
private record Wild(HunterEntity mob,Cow victim,BlockPos at) { void close(){mob.discard();victim.discard();} }
|
||||
private static BlockPos arena(GameTestHelper h,int slot) {
|
||||
var at=h.absolutePos(new BlockPos(0,74+slot*10,0));
|
||||
for(int x=-5;x<=5;x++)for(int z=-3;z<=18;z++) {
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,-1,z),Blocks.STONE.defaultBlockState());
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,7,z),Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
return at;
|
||||
}
|
||||
private static Wild wild(GameTestHelper h,int slot) {
|
||||
var at=arena(h,slot);var mob=Infected.HUNTER.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setPos(Vec3.atBottomCenterOf(at));mob.setOnGround(true);
|
||||
var target=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.getAttribute(Attributes.MAX_HEALTH).setBaseValue(100);target.setHealth(100);target.setPos(Vec3.atBottomCenterOf(at.offset(0,0,8)));
|
||||
h.getLevel().addFreshEntity(target);h.getLevel().addFreshEntity(mob);mob.setTarget(target);return new Wild(mob,target,at);
|
||||
}
|
||||
private static void tick(GameTestHelper h,Entity entity) { h.getLevel().tickNonPassenger(entity); }
|
||||
private static void launch(GameTestHelper h,Wild w) { for(int i=0;i<32&&w.mob.phase()!=HunterEntity.Phase.LEAPING;i++)tick(h,w.mob);h.assertTrue(w.mob.phase()==HunterEntity.Phase.LEAPING,"Native Hunter goal reaches takeoff"); }
|
||||
@GameTest public void eggProfileSpawnAndAdultRule(GameTestHelper h) {
|
||||
var egg=new ItemStack(Infected.HUNTER_EGG);var mob=Infected.HUNTER.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setBaby(true);
|
||||
h.assertTrue(SpawnEggItem.getType(egg)==Infected.HUNTER&&SpawnEggItem.byId(Infected.HUNTER).orElseThrow().value()==Infected.HUNTER_EGG,"Native egg resolves Hunter in both directions");
|
||||
h.assertTrue(!mob.isBaby()&&mob.getMaxHealth()==10&&mob.getAttributeValue(Attributes.MOVEMENT_SPEED)==.30,"Hunter rejects babies and has its own fast attributes");
|
||||
var profile=FamiliarCatalog.bundled().forEgg(egg);h.assertTrue(profile.id().equals("sanctuary:hunter")&&profile.health()==12&&FamiliarPounce.applies(profile.signature()),"Dedicated Hunter profile supplies the real pounce");
|
||||
var attribute=net.minecraft.world.attribute.EnvironmentAttributes.NATURAL_MOB_SPAWNS;
|
||||
for(var b:h.getLevel().getChunkSource().getGenerator().getBiomeSource().possibleBiomes()) {
|
||||
var entries=b.value().getAttributes().applyModifier(attribute,attribute.defaultValue()).getMobsToSpawn(MobCategory.MONSTER).unwrap();
|
||||
if(entries.stream().noneMatch(e->e.value().type()==EntityTypes.ZOMBIE))continue;
|
||||
var found=entries.stream().filter(e->e.value().type()==Infected.HUNTER).toList();
|
||||
h.assertTrue(found.size()==1&&found.getFirst().weight()==4&&found.getFirst().value().count().minInclusive()==1&&found.getFirst().value().count().maxInclusive()==1,"Zombie habitats receive one rare singleton Hunter entry");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void nativeWindupAirborneContactAndRecoveryDealOneHit(GameTestHelper h) {
|
||||
var w=wild(h,1);var start=w.mob.position();for(int i=0;i<8;i++)tick(h,w.mob);
|
||||
h.assertTrue(w.mob.phase()==HunterEntity.Phase.WINDUP&&w.mob.position().subtract(start).length()<.01&&w.mob.getPose()==Pose.CROUCHING,"Hunter crouches and holds while preparing");
|
||||
launch(h,w);double top=w.mob.getY();for(int i=0;i<30&&w.mob.phase()==HunterEntity.Phase.LEAPING;i++){top=Math.max(top,w.mob.getY());w.victim.setInvulnerableTime(0);tick(h,w.mob);}
|
||||
h.assertTrue(top>start.y+1&&w.victim.getHealth()==96&&w.mob.phase()==HunterEntity.Phase.RECOVERING,"The native airborne body reaches contact and deals four damage: hp="+w.victim.getHealth()+" top="+top+" z="+w.mob.getZ()+" target="+w.victim.position());
|
||||
for(int i=0;i<10;i++){w.victim.setInvulnerableTime(0);tick(h,w.mob);}
|
||||
h.assertTrue(w.victim.getHealth()==96&&!w.mob.doHurtTarget(h.getLevel(),w.victim)&&w.mob.pounceCooldown()>0,"Recovery prevents both repeated contact damage and native melee double-hits");w.close();h.succeed();
|
||||
}
|
||||
@GameTest public void sidestepAvoidsTheAirborneBody(GameTestHelper h) {
|
||||
var w=wild(h,2);launch(h,w);double x=w.mob.getX();w.victim.setPos(w.victim.position().add(4,0,0));
|
||||
for(int i=0;i<35&&w.mob.phase()==HunterEntity.Phase.LEAPING;i++)tick(h,w.mob);
|
||||
h.assertTrue(w.victim.getHealth()==100&&Math.abs(w.mob.getX()-x)<.01&&w.mob.getZ()>w.at.getZ()+5,"A released pounce keeps its original trajectory and misses a side step");
|
||||
h.assertTrue(w.mob.phase()==HunterEntity.Phase.RECOVERING&&w.mob.pounceCooldown()>0,"A miss still lands in recovery and pays cooldown");w.close();h.succeed();
|
||||
}
|
||||
@GameTest public void wallStopsRealBodyWithoutDamageThroughIt(GameTestHelper h) {
|
||||
var w=wild(h,3);launch(h,w);for(int x=-4;x<=4;x++)for(int y=0;y<6;y++)h.getLevel().setBlockAndUpdate(w.at.offset(x,y,3),Blocks.STONE.defaultBlockState());
|
||||
for(int i=0;i<25&&w.mob.phase()==HunterEntity.Phase.LEAPING;i++)tick(h,w.mob);
|
||||
h.assertTrue(w.victim.getHealth()==100&&w.mob.getZ()<=w.at.getZ()+2.701&&w.mob.phase()==HunterEntity.Phase.RECOVERING,"Native collision ends the pounce before the wall instead of hitting through it");
|
||||
h.assertTrue(h.getLevel().getBlockState(w.at.offset(0,1,3)).is(Blocks.STONE),"Hunter never breaks the wall");w.close();h.succeed();
|
||||
}
|
||||
@GameTest public void lowCeilingStopsTheJumpAndGravityResumes(GameTestHelper h) {
|
||||
var w=wild(h,4);for(int x=-2;x<=2;x++)for(int z=-1;z<=5;z++)h.getLevel().setBlockAndUpdate(w.at.offset(x,3,z),Blocks.STONE.defaultBlockState());launch(h,w);
|
||||
for(int i=0;i<8&&w.mob.phase()==HunterEntity.Phase.LEAPING;i++)tick(h,w.mob);
|
||||
h.assertTrue(w.mob.phase()==HunterEntity.Phase.RECOVERING&&w.victim.getHealth()==100&&w.mob.getY()<=w.at.getY()+1.051,"A ceiling collides with the actual head/body");
|
||||
for(int i=0;i<15;i++)tick(h,w.mob);h.assertTrue(w.mob.onGround()&&!w.mob.isNoGravity(),"Hunter falls back to the floor without a hovering recovery");w.close();h.succeed();
|
||||
}
|
||||
@GameTest public void reloadDropsPreparationAndFlightButRetainsCooldown(GameTestHelper h) {
|
||||
var w=wild(h,5);w.mob.setHealth(7);for(int i=0;i<8;i++)tick(h,w.mob);assertReload(h,w.mob);
|
||||
launch(h,w);assertReload(h,w.mob);w.close();h.succeed();
|
||||
}
|
||||
private static void assertReload(GameTestHelper h,HunterEntity original) {
|
||||
var out=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());original.save(out);var tag=out.buildResult();tag.putBoolean("IsBaby",true);
|
||||
var loaded=EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),tag),h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof HunterEntity hunter&&!hunter.isBaby()&&hunter.getUUID().equals(original.getUUID())&&hunter.getHealth()==7&&hunter.phase()==HunterEntity.Phase.IDLE&&hunter.pounceCooldown()==120&&hunter.getDeltaMovement().equals(Vec3.ZERO)&&hunter.getPose()==Pose.STANDING,"Reload preserves identity/health/cooldown without replaying the jump or baby pose");
|
||||
}
|
||||
@GameTest public void teleportAcrossTargetCannotBecomeASweptPounceHit(GameTestHelper h) {
|
||||
var w=wild(h,6);launch(h,w);w.mob.teleportTo(w.mob.getX(),w.mob.getY(),w.mob.getZ()+12);tick(h,w.mob);
|
||||
h.assertTrue(w.victim.getHealth()==100&&w.mob.phase()==HunterEntity.Phase.RECOVERING&&w.mob.pounceCooldown()>0,"A discontinuous relocation cancels contact and retains spent cooldown");w.close();h.succeed();
|
||||
}
|
||||
@GameTest public void unloadedFlightCorridorIsRefusedWithoutGeneration(GameTestHelper h) {
|
||||
var w=wild(h,7);launch(h,w);var remote=w.mob.blockPosition().offset(10000,0,0);h.assertTrue(!h.getLevel().hasChunkAt(remote),"Remote corridor starts unloaded");
|
||||
w.mob.setDeltaMovement(10000,0,0);tick(h,w.mob);
|
||||
h.assertTrue(w.mob.phase()==HunterEntity.Phase.RECOVERING&&!h.getLevel().hasChunkAt(remote)&&w.victim.getHealth()==100,"Full flight corridor guard rejects an unloaded jump without generating terrain");w.close();h.succeed();
|
||||
}
|
||||
@GameTest public void familiarGActuallyJumpsAndHitsOnlyOnPhysicalContact(GameTestHelper h) {
|
||||
var p=pet(h,8);var pet=FamiliarService.active(p);var enemy=enemy(h,pet.position().add(0,0,7));FamiliarBattle.engage(p,enemy,false);
|
||||
var result=FamiliarBattle.activate(p);h.assertTrue(result.equals("activated"),"Native G activation starts Hunter preparation: "+result+" active="+FamiliarBattle.active(p)+" lookup="+p.level().getEntity(enemy.getUUID())+" mayHit="+FamiliarBattle.mayHit(pet,enemy));var cast=FamiliarBattle.state(p).cast;double start=pet.getY(),top=start;
|
||||
for(int i=0;i<12;i++)tick(h,pet);h.assertTrue(enemy.getHealth()==20&&pet.getY()<start+.01,"Preparation cannot apply remote damage");
|
||||
for(int i=0;i<70&&FamiliarBattle.state(p).cast!=null;i++){top=Math.max(top,pet.getY());enemy.setInvulnerableTime(0);tick(h,pet);}
|
||||
h.assertTrue(top>start+1&&enemy.getHealth()<20&&enemy.getHealth()>=16&&cast.hit.size()==1,"The actual miniature body jumps and applies one shared hit: top="+top+" hp="+enemy.getHealth()+" pos="+pet.position()+" enemy="+enemy.position());
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&CompanionService.record(p).readyAt()==FamiliarBattle.bond(p).readyAt()&&FamiliarBattle.state(p).cast==null,"Released G commits shared durable cooldown and finishes recovery");enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarCanBeDodgedAndStillPaysCooldown(GameTestHelper h) {
|
||||
var p=pet(h,9);var pet=FamiliarService.active(p);var enemy=enemy(h,pet.position().add(0,0,7));FamiliarBattle.engage(p,enemy,false);FamiliarBattle.activate(p);
|
||||
for(int i=0;i<30&&pet.getY()<p.getY()+.2;i++)tick(h,pet);double x=pet.getX();enemy.setPos(enemy.position().add(4,0,0));run(h,p,70);
|
||||
h.assertTrue(enemy.getHealth()==20&&Math.abs(pet.getX()-x)<.01&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&FamiliarBattle.state(p).cast==null,"The familiar's released ballistic trajectory never homes in on a sidestep");enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarWallAndCancelCannotProduceRemoteDamage(GameTestHelper h) {
|
||||
var p=pet(h,10);var pet=FamiliarService.active(p);var enemy=enemy(h,pet.position().add(0,0,7));FamiliarBattle.engage(p,enemy,false);FamiliarBattle.activate(p);
|
||||
for(int i=0;i<30&&pet.getY()<p.getY()+.2;i++)tick(h,pet);var wall=p.blockPosition().offset(0,0,3);
|
||||
for(int x=-3;x<=3;x++)for(int y=0;y<6;y++)p.level().setBlockAndUpdate(wall.offset(x,y,0),Blocks.STONE.defaultBlockState());run(h,p,60);
|
||||
h.assertTrue(enemy.getHealth()==20&&pet.getZ()<wall.getZ()&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"A wall stops the moved familiar without a remote hit or cooldown refund");
|
||||
FamiliarBattle.cancel(p);h.assertTrue(pet.getPose()==Pose.STANDING&&FamiliarBattle.state(p).cast==null,"Normal cancellation clears preparation and recovery pose");enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarTeleportDuringFlightCannotHitAcrossTheTarget(GameTestHelper h) {
|
||||
var p=pet(h,11);var pet=FamiliarService.active(p);var enemy=enemy(h,pet.position().add(0,0,7));FamiliarBattle.engage(p,enemy,false);FamiliarBattle.activate(p);
|
||||
for(int i=0;i<30&&pet.getY()<p.getY()+.2;i++)tick(h,pet);pet.teleportTo(pet.getX(),pet.getY(),pet.getZ()+10);run(h,p,40);
|
||||
h.assertTrue(enemy.getHealth()==20&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"Owner recall or teleport across an enemy cannot become body-sweep damage");enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void ownerAndPassiveAnimalsCannotBePounceVictims(GameTestHelper h) {
|
||||
var p=pet(h,12);var pet=FamiliarService.active(p);var cow=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);cow.setNoAi(true);cow.setPos(pet.position().add(0,0,5));p.level().addFreshEntity(cow);
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,cow,cow.position());FamiliarBattle.state(p).cast=cast;run(h,p,40);
|
||||
h.assertTrue(cow.getHealth()==10&&cast.hit.isEmpty(),"Even a server-created cast cannot bypass passive-animal protection");
|
||||
float hp=p.getHealth();cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,p,p.position());FamiliarBattle.state(p).cast=cast;run(h,p,20);
|
||||
h.assertTrue(p.getHealth()==hp&&cast.hit.isEmpty(),"The Hunter cannot pounce on its owner");cow.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void protectedTakeoffIsRefusedBeforeCooldown(GameTestHelper h) {
|
||||
var p=pet(h,13);var pet=FamiliarService.active(p);var enemy=enemy(h,pet.position().add(0,0,7));FamiliarBattle.engage(p,enemy,false);FamiliarBattle.activate(p);var start=pet.position();
|
||||
try { DENIED.set(BlockPos.containing(start.add(HunterJump.impulse(start,enemy.position()))));run(h,p,40);h.assertTrue(pet.position().subtract(start).horizontalDistance()<.01&&enemy.getHealth()==20&&FamiliarBattle.bond(p).readyAt()==0,"A forbidden first flight cell prevents takeoff before cooldown and damage"); }
|
||||
finally { DENIED.set(null); }
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void everyMiniatureSizeRetainsAdultGeometryAndIdentity(GameTestHelper h) {
|
||||
var p=pet(h,14);var id=FamiliarBattle.bond(p).id();
|
||||
for(int value:new int[]{120,500,1000,1500,3000,6000}) {
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount+=10;FamiliarService.tick(p);
|
||||
var pet=FamiliarService.active(p);h.assertTrue(pet!=null&&!pet.babyAppearance()&&Math.abs(pet.visualHeight()-.85*value/1000)<.001&&FamiliarBattle.bond(p).id().equals(id),"Every supported scale remains an adult miniature with the same bond: "+value);
|
||||
}
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
private static ServerPlayer pet(GameTestHelper h,int slot) {
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIE);p.setPos(Vec3.atBottomCenterOf(arena(h,slot)));var egg=new ItemStack(Infected.HUNTER_EGG);var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount=200;FamiliarService.tick(p);
|
||||
var pet=FamiliarService.active(p);pet.setPos(p.position());pet.setOnGround(true);return p;
|
||||
}
|
||||
private static net.minecraft.world.entity.monster.zombie.Zombie enemy(GameTestHelper h,Vec3 at) { var target=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(at);h.getLevel().addFreshEntity(target);return target; }
|
||||
private static void run(GameTestHelper h,ServerPlayer p,int ticks) { for(int i=0;i<ticks&&FamiliarBattle.state(p).cast!=null;i++)tick(h,FamiliarService.active(p)); }
|
||||
private static void cleanup(ServerPlayer p) { AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);CompanionService.clear(p); }
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.tags.EntityTypeTags;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.context.UseOnContext;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Native egg, biome, entity persistence and familiar combat integration. */
|
||||
public final class Infected280GameTests {
|
||||
@GameTest public void infectedHasDistinctStatsAndNativeMelee(GameTestHelper h) {
|
||||
var mob=Infected.TYPE.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
var zombie=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
h.assertTrue(mob.getMaxHealth()==10&&mob.getHealth()==10,"Infected starts with ten health");
|
||||
h.assertTrue(mob.getAttributeBaseValue(Attributes.MOVEMENT_SPEED)>zombie.getAttributeBaseValue(Attributes.MOVEMENT_SPEED),"Infected is faster than a vanilla zombie");
|
||||
var victim=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
float health=victim.getHealth();
|
||||
h.assertTrue(mob.doHurtTarget(h.getLevel(),victim)&&victim.getHealth()<health,"Inherited zombie melee causes actual damage");
|
||||
float adult=mob.getBbHeight();mob.setBaby(true);
|
||||
h.assertTrue(!mob.isBaby()&&mob.getBbHeight()==adult&&mob.getAttributeValue(Attributes.MOVEMENT_SPEED)==.25,"Infected rejects baby dimensions and the native baby speed bonus");
|
||||
zombie.setBaby(true);h.assertTrue(zombie.isBaby()&&zombie.getBbHeight()<adult,"Vanilla zombie babies remain available");
|
||||
h.assertTrue(mob.getType().builtInRegistryHolder().is(EntityTypeTags.UNDEAD)&&mob.getType().builtInRegistryHolder().is(EntityTypeTags.BURN_IN_DAYLIGHT),"Undead and daylight tags resolve on the registered type");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void actualEggUseSpawnsOnlyInfected(GameTestHelper h) {
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIE);
|
||||
var at=p.blockPosition().offset(3,-1,0);
|
||||
p.setItemInHand(InteractionHand.MAIN_HAND,new ItemStack(Infected.SPAWN_EGG,2));
|
||||
var egg=p.getMainHandItem();
|
||||
h.assertTrue(SpawnEggItem.getType(egg)==Infected.TYPE&&SpawnEggItem.byId(Infected.TYPE).orElseThrow().value()==Infected.SPAWN_EGG,"Native bidirectional egg mapping resolves the custom type");
|
||||
Infected.SPAWN_EGG.useOn(new UseOnContext(p,InteractionHand.MAIN_HAND,
|
||||
new BlockHitResult(Vec3.atCenterOf(at).add(0,.5,0),Direction.UP,at,false)));
|
||||
var found=h.getLevel().getEntitiesOfClass(InfectedEntity.class,new net.minecraft.world.phys.AABB(at).inflate(2));
|
||||
h.assertTrue(found.size()==1&&egg.getCount()==1,"A real survival egg use spawns one infected and consumes one egg");
|
||||
found.forEach(Entity::discard);cleanup(p);h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void nativeSaveRetainsInfectedIdentityAndHealth(GameTestHelper h) {
|
||||
var mob=Infected.TYPE.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setHealth(7);mob.setBaby(true);
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());
|
||||
h.assertTrue(mob.save(output),"The infected is a persistent entity");
|
||||
var saved=output.buildResult();saved.putBoolean("IsBaby",true);
|
||||
var input=TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),saved);
|
||||
var loaded=EntityType.loadEntityRecursive(input,h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof InfectedEntity infected&&infected.getType()==Infected.TYPE
|
||||
&&infected.getHealth()==7&&!infected.isBaby()&&infected.getUUID().equals(mob.getUUID()),"Native reload rejects an old baby flag while retaining type, UUID and health");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void naturalSpawnTablesRespectExistingZombieHabitats(GameTestHelper h) {
|
||||
var biomes=h.getLevel().registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
// Fabric's 26.3 flat_all_dimensions fixture uses a desert Overworld.
|
||||
var habitats=h.getLevel().getChunkSource().getGenerator().getBiomeSource().possibleBiomes().stream()
|
||||
.filter(b->spawns(b.value()).stream().anyMatch(e->e.value().type()==EntityTypes.ZOMBIE)).toList();
|
||||
h.assertTrue(!habitats.isEmpty(),"The native fixture includes a real zombie habitat");
|
||||
for(var biome:habitats) {
|
||||
var entries=spawns(biome.value());
|
||||
var infected=entries.stream().filter(e->e.value().type()==Infected.TYPE).toList();
|
||||
h.assertTrue(infected.size()==1&&infected.getFirst().weight()==15,"Zombie habitat gets exactly one low-weight infected entry: "+biome+" "+infected);
|
||||
}
|
||||
for(var key:java.util.List.of(Biomes.MUSHROOM_FIELDS,Biomes.NETHER_WASTES,Biomes.THE_END))
|
||||
h.assertTrue(spawns(biomes.getOrThrow(key).value()).stream()
|
||||
.noneMatch(e->e.value().type()==Infected.TYPE),"No infected in mushroom fields, Nether or End");
|
||||
h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void immersionKeepsTheInfectedSpecies(GameTestHelper h) {
|
||||
var at=h.absolutePos(new BlockPos(2,2,2));
|
||||
for(int x=-1;x<=2;x++)for(int y=0;y<=2;y++)for(int z=-1;z<=1;z++)
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,y,z),Blocks.WATER.defaultBlockState());
|
||||
var infected=Infected.TYPE.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
var zombie=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
infected.setPos(Vec3.atBottomCenterOf(at));zombie.setPos(Vec3.atBottomCenterOf(at.east()));
|
||||
infected.setInWaterTime(601);zombie.setInWaterTime(601);
|
||||
h.getLevel().addFreshEntity(infected);h.getLevel().addFreshEntity(zombie);
|
||||
h.runAfterDelay(5,()->{
|
||||
h.assertTrue(zombie.isUnderWaterConverting(),"The water fixture starts vanilla zombie conversion");
|
||||
h.assertTrue(infected.isAlive()&&!infected.isUnderWaterConverting()&&infected.getType()==Infected.TYPE,"The infected retains its species under the same immersion");
|
||||
infected.discard();zombie.discard();h.succeed();
|
||||
});
|
||||
}
|
||||
|
||||
@GameTest public void equippedInfectedUsesOwnProfileAndPersistentBond(GameTestHelper h) {
|
||||
var p=equipped(h);var pet=FamiliarService.active(p);var profile=FamiliarBattle.profile(p);var id=FamiliarBattle.bond(p).id();
|
||||
h.assertTrue(pet!=null&&pet.owner()==p&&!pet.babyAppearance(),"Equipping the new egg produces an adult miniature familiar");
|
||||
h.assertTrue(profile.id().equals("sanctuary:infected")&&pet.getMaxHealth()==10&&pet.getHealth()==10,"Dedicated profile supplies actual familiar health");
|
||||
h.assertTrue(FamiliarCatalog.bundled().forEgg(FamiliarBattle.egg(p)).equals(profile),"Client bundled and server loaded profiles agree");
|
||||
h.assertTrue(pet.burnsInDaylight(),"The infected familiar shares the undead sunlight rule");
|
||||
FamiliarBattle.command(p,"work");h.assertTrue(!FamiliarBattle.state(p).workMode,"Unsupported work mode is refused by the server");
|
||||
FamiliarBattle.update(p,FamiliarBattle.bond(p).health(6,0));
|
||||
var egg=FamiliarBattle.egg(p).copy();cleanup(p);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount+=10;FamiliarService.tick(p);
|
||||
h.assertTrue(FamiliarService.active(p)!=null&&FamiliarBattle.bond(p).id().equals(id)&&FamiliarService.active(p).getHealth()==6,"Re-equipping retains this individual's identity and health");
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void infectedFamiliarBiteCausesRealDamage(GameTestHelper h) {
|
||||
var p=equipped(h);var pet=FamiliarService.active(p);
|
||||
var enemy=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);enemy.setNoAi(true);
|
||||
enemy.setPos(pet.position().add(0,0,1));h.getLevel().addFreshEntity(enemy);
|
||||
float health=enemy.getHealth();
|
||||
var bite=FamiliarBattle.profile(p).initiative();
|
||||
var cast=new FamiliarTechniques.Cast(pet,bite,true,enemy,enemy.position());
|
||||
h.assertTrue(FamiliarActions.validate(cast).isEmpty(),"The infected can reach a nearby hostile");
|
||||
FamiliarActions.execute(cast);
|
||||
h.assertTrue(enemy.getHealth()<health&&FamiliarBattle.fighting(p),"The distinct profile deals native damage and enters combat");
|
||||
enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void eggSwapStopsThePreviousGoalBeforeTheNewBondExists(GameTestHelper h) {
|
||||
var p=equipped(h);var old=FamiliarService.active(p);var id=FamiliarBattle.bond(p).id();
|
||||
old.setAggressive(true);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,new ItemStack(Items.ZOMBIE_SPAWN_EGG));
|
||||
h.assertTrue(FamiliarBattle.profile(p)!=null&&FamiliarBattle.bond(p)==null,"A fresh replacement egg has a profile before it has an individual");
|
||||
FamiliarBrain.tick(old);
|
||||
h.assertTrue(!old.isAggressive(),"The previous running goal safely stops during that native tick window");
|
||||
FamiliarService.tick(p);
|
||||
h.assertTrue(old.isRemoved()&&FamiliarService.active(p)!=null&&!FamiliarBattle.bond(p).id().equals(id)
|
||||
&&FamiliarBattle.profile(p).id().equals("sanctuary:zombie"),"Normal reconciliation creates the replacement without reusing the old individual");
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void adultMiniaturePreservesVariableSizeAndIndividual(GameTestHelper h) {
|
||||
var p=equipped(h);var id=FamiliarBattle.bond(p).id();var model=Infected.TYPE.create(h.getLevel(),EntitySpawnReason.MOB_SUMMONED);
|
||||
model.setBaby(true);
|
||||
for(int value:new int[]{120,500,1000,1500,3000,6000}) {
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();
|
||||
size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount+=10;FamiliarService.tick(p);
|
||||
var pet=FamiliarService.active(p);
|
||||
h.assertTrue(pet!=null&&!pet.babyAppearance()&&FamiliarBattle.bond(p).id().equals(id),"Every miniature size retains adult proportions and the same individual");
|
||||
h.assertTrue(FamiliarSize.value(FamiliarBattle.egg(p))==value,"The stored size trait is never rerolled or rewritten");
|
||||
double height=model.getBbHeight()*FamiliarSize.renderScale(model,egg);
|
||||
h.assertTrue(Math.abs(pet.visualHeight()-height)<.0001&&Math.abs(height-.85*value/1000)<.0001,"Voice, visible size and mount height all use the adult miniature scale");
|
||||
}
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
|
||||
private static java.util.List<net.minecraft.util.random.Weighted<net.minecraft.world.level.biome.MobSpawnSettings.SpawnerData>> spawns(net.minecraft.world.level.biome.Biome biome) {
|
||||
var attribute=net.minecraft.world.attribute.EnvironmentAttributes.NATURAL_MOB_SPAWNS;
|
||||
return biome.getAttributes().applyModifier(attribute,attribute.defaultValue()).getMobsToSpawn(MobCategory.MONSTER).unwrap();
|
||||
}
|
||||
private static net.minecraft.server.level.ServerPlayer equipped(GameTestHelper h) {
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIE);
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,new ItemStack(Infected.SPAWN_EGG));
|
||||
p.tickCount=200;FamiliarService.tick(p);return p;
|
||||
}
|
||||
private static void cleanup(net.minecraft.server.level.ServerPlayer p) {
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);
|
||||
FamiliarService.tick(p);FamiliarBattle.release(p);CompanionService.clear(p);
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -66,7 +66,19 @@ public final class Property270GameTests {
|
||||
PropertyService.recover(p);h.assertTrue(FamiliarExperience.playerXp(p)==250,"Repeated recovery does not debit again");
|
||||
var reloaded=new PropertyStore(server.getWorldPath(net.minecraft.world.level.storage.LevelResource.ROOT).resolve("sanctuary/properties-v1.json"),server.overworld().getSeed());
|
||||
h.assertTrue(reloaded.get(p.getUUID()).equals(store.get(p.getUUID())),"Committed property survives disk reload");
|
||||
for(String alias:java.util.List.of("property","propriete","propriété"))h.assertTrue(server.getCommands().getDispatcher().getRoot().getChild("sanctuary").getChild(alias).canUse(p.createCommandSourceStack()),"Property commands need no operator");
|
||||
var dispatcher=server.getCommands().getDispatcher();var source=p.createCommandSourceStack();
|
||||
var root=dispatcher.getRoot().getChild("sanctuary");var property=root.getChild("property");
|
||||
h.assertTrue(property!=null&&property.canUse(source)&&property.getCommand()!=null,"English property command opens the menu without operator permissions");
|
||||
for(String action:java.util.List.of("claim","protect","duel"))h.assertTrue(property.getChild(action)!=null&&property.getChild(action).canUse(source),"English property command retains "+action);
|
||||
var suggestions=dispatcher.getCompletionSuggestions(dispatcher.parse("sanctuary prop",source)).join().getList().stream().map(s->s.getText()).toList();
|
||||
h.assertTrue(suggestions.equals(java.util.List.of("property")),"Property completion offers only the English command");
|
||||
for(String alias:java.util.List.of("propriete","propriété")){
|
||||
h.assertTrue(root.getChild(alias)==null,"Removed property alias is absent: "+alias);
|
||||
for(String suffix:java.util.List.of(""," claim"," protect"," duel 00000000-0000-0000-0000-000000000000")){
|
||||
boolean rejected=false;try{dispatcher.execute("sanctuary "+alias+suffix,source);}catch(com.mojang.brigadier.exceptions.CommandSyntaxException expected){rejected=true;}
|
||||
h.assertTrue(rejected,"Removed property alias cannot execute: "+alias+suffix);
|
||||
}
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void spawnDimensionAndHeightCannotBeClaimed(GameTestHelper h)throws Exception{
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.zipline.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Real projectile intersections, whole-body zipline motion and interruption rules. */
|
||||
public final class Smoker285GameTests {
|
||||
private static final java.util.concurrent.atomic.AtomicReference<BlockPos> DENIED=new java.util.concurrent.atomic.AtomicReference<>();
|
||||
static { CompanionWorld.ALLOW.register((player,pos,operation)->!pos.equals(DENIED.get())); }
|
||||
private static BlockPos arena(GameTestHelper h,int slot) {
|
||||
// Stay inside the framework's tracked chunks so native projectile queries see players immediately.
|
||||
var at=h.absolutePos(new BlockPos(0,70+slot*9,0));
|
||||
for(int x=-4;x<=4;x++)for(int z=-3;z<=18;z++) {
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,-1,z),Blocks.STONE.defaultBlockState());
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,7,z),Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
return at;
|
||||
}
|
||||
private static SmokerEntity smoker(GameTestHelper h,BlockPos at) {
|
||||
var mob=Infected.SMOKER.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setNoAi(true);mob.setPos(Vec3.atBottomCenterOf(at));mob.setOnGround(true);h.getLevel().addFreshEntity(mob);return mob;
|
||||
}
|
||||
private static ServerPlayer rider(GameTestHelper h,BlockPos at) {
|
||||
var p=Progression003GameTests.player(h);p.setUUID(java.util.UUID.randomUUID());p.connection.handleAcceptPlayerLoad(new net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket());
|
||||
p.setPos(Vec3.atBottomCenterOf(at));p.setOnGround(true);p.setInvulnerableTime(0);h.getLevel().addNewPlayer(p);return p;
|
||||
}
|
||||
private static void tick(GameTestHelper h,Entity entity) { h.getLevel().tickNonPassenger(entity); }
|
||||
private static void catchTarget(GameTestHelper h,SmokerLassoEntity rope) { for(int i=0;i<28&&!rope.isRemoved()&&!rope.isReeling();i++)tick(h,rope); }
|
||||
private static void ride(GameTestHelper h,SmokerLassoEntity rope) { for(int i=0;i<65&&!rope.isRemoved();i++)tick(h,rope); }
|
||||
@GameTest public void eggProfileSpawnAndAdultRule(GameTestHelper h) {
|
||||
var egg=new ItemStack(Infected.SMOKER_EGG);var mob=Infected.SMOKER.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setBaby(true);
|
||||
h.assertTrue(SpawnEggItem.getType(egg)==Infected.SMOKER&&SpawnEggItem.byId(Infected.SMOKER).orElseThrow().value()==Infected.SMOKER_EGG,"Native egg maps to Smoker in both directions");
|
||||
h.assertTrue(!mob.isBaby()&&mob.getMaxHealth()==10&&mob.getAttributeValue(Attributes.MOVEMENT_SPEED)==.23,"Smoker always has adult proportions and its own attributes");
|
||||
var profile=FamiliarCatalog.bundled().forEgg(egg);h.assertTrue(profile.id().equals("sanctuary:smoker")&&profile.health()==12&&profile.signature().effect().equals("lasso"),"Dedicated familiar profile is bundled");
|
||||
var attribute=net.minecraft.world.attribute.EnvironmentAttributes.NATURAL_MOB_SPAWNS;
|
||||
for(var b:h.getLevel().getChunkSource().getGenerator().getBiomeSource().possibleBiomes()) {
|
||||
var entries=b.value().getAttributes().applyModifier(attribute,attribute.defaultValue()).getMobsToSpawn(MobCategory.MONSTER).unwrap();
|
||||
if(entries.stream().noneMatch(e->e.value().type()==EntityTypes.ZOMBIE))continue;
|
||||
var found=entries.stream().filter(e->e.value().type()==Infected.SMOKER).toList();
|
||||
h.assertTrue(found.size()==1&&found.getFirst().weight()==4&&found.getFirst().value().count().minInclusive()==1&&found.getFirst().value().count().maxInclusive()==1,"Zombie habitats receive a rare singleton Smoker");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void actualPlayerMustBeHitBeforeZiplineMotionBegins(GameTestHelper h) {
|
||||
var at=arena(h,0);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));var initial=p.position();var rope=SmokerLassoEntity.launch(source,p,null);
|
||||
tick(h,rope);h.assertTrue(!rope.isReeling()&&p.position().equals(initial),"Throwing the lasso alone cannot move the player");catchTarget(h,rope);
|
||||
h.assertTrue(rope.isReeling()&&rope.caughtEntity()==p&&SmokerLassoEntity.pulled(p)&&p.position().equals(initial),"A real native projectile intersection attaches the player before travel");
|
||||
while(!rope.isRemoved()) { var before=p.position();tick(h,rope);h.assertTrue(p.position().distanceTo(before)<=ZiplineMotion.SPEED+.0001,"Each step uses the existing 0.58-block zipline speed"); }
|
||||
h.assertTrue(Math.abs(p.distanceTo(source)-1.8)<.001&&!SmokerLassoEntity.pulled(p)&&SmokerLassoEntity.active(source)==null&&!p.isNoGravity(),"Arrival releases the cable near the Smoker with ordinary gravity");
|
||||
source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void nativeAiPreparesLaunchesAndCommitsCooldown(GameTestHelper h) {
|
||||
var at=arena(h,1);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));source.setNoAi(false);source.setTarget(p);
|
||||
for(int i=0;i<10;i++)tick(h,source);
|
||||
h.assertTrue(source.windingUp()&&SmokerLassoEntity.active(source)==null,"Native goal visibly prepares before launching");
|
||||
for(int i=0;i<20&&SmokerLassoEntity.active(source)==null;i++)tick(h,source);
|
||||
var rope=SmokerLassoEntity.active(source);h.assertTrue(rope!=null&&source.lassoCooldown()==160,"Release commits cooldown immediately");
|
||||
catchTarget(h,rope);var before=p.position();ride(h,rope);
|
||||
h.assertTrue(p.getZ()<before.z-5&&source.lassoCooldown()>0,"The native attack really reels its victim without an instant grab");source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void sidesteppingDuringFlightReallyDodgesTheLasso(GameTestHelper h) {
|
||||
var at=arena(h,2);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));source.setNoAi(false);source.setTarget(p);
|
||||
for(int i=0;i<30&&SmokerLassoEntity.active(source)==null;i++)tick(h,source);var rope=SmokerLassoEntity.active(source);
|
||||
tick(h,rope);tick(h,rope);p.setPos(p.position().add(3,0,0));var dodged=p.position();ride(h,rope);
|
||||
h.assertTrue(rope.isRemoved()&&!SmokerLassoEntity.pulled(p)&&p.position().equals(dodged)&&source.lassoCooldown()>0,"The released lasso never homes in on a sidestep, and a miss still costs cooldown");source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void wallInsertedDuringFlightStopsTheProjectile(GameTestHelper h) {
|
||||
var at=arena(h,3);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));var initial=p.position();var rope=SmokerLassoEntity.launch(source,p,null);tick(h,rope);
|
||||
for(int x=-3;x<=3;x++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,5),Blocks.STONE.defaultBlockState());ride(h,rope);
|
||||
h.assertTrue(rope.isRemoved()&&!SmokerLassoEntity.pulled(p)&&p.position().equals(initial)&&h.getLevel().getBlockState(at.offset(0,1,5)).is(Blocks.STONE),"A native block collision prevents capture and preserves the wall");source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void wholePlayerBodyHitsLowObstacleEvenWhenEyesCanSeeOverIt(GameTestHelper h) {
|
||||
var at=arena(h,4);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));var rope=SmokerLassoEntity.launch(source,p,null);catchTarget(h,rope);
|
||||
h.assertTrue(rope.isReeling(),"The physical lasso first attaches");h.getLevel().setBlockAndUpdate(at.offset(0,0,5),Blocks.STONE.defaultBlockState());
|
||||
h.assertTrue(source.hasLineOfSight(p),"The eye-level line still passes over the low obstacle");ride(h,rope);
|
||||
h.assertTrue(p.getZ()>=at.getZ()+6.3&&!SmokerLassoEntity.pulled(p)&&p.getDeltaMovement().y<=-.18&&!p.isNoGravity(),"The complete body stops before the block and the cable releases into a normal fall");source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void hurtingTheSmokerReleasesItsVictim(GameTestHelper h) {
|
||||
var at=arena(h,5);var source=smoker(h,at);source.setHealth(5);var p=rider(h,at.offset(0,0,10));var rope=SmokerLassoEntity.launch(source,p,null);catchTarget(h,rope);tick(h,rope);var before=p.position();
|
||||
source.setHealth(10);source.hurtServer(h.getLevel(),source.damageSources().generic(),1);tick(h,rope);
|
||||
h.assertTrue(rope.isRemoved()&&!SmokerLassoEntity.pulled(p)&&p.position().equals(before),"Actual source damage cuts an established cable before another travel step");source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void cuttingHookAndRemovingSourceCleanBothAttachments(GameTestHelper h) {
|
||||
var at=arena(h,6);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));var rope=SmokerLassoEntity.launch(source,p,null);catchTarget(h,rope);
|
||||
h.assertTrue(rope.hurtServer(h.getLevel(),source.damageSources().playerAttack(p),1)&&!SmokerLassoEntity.pulled(p)&&SmokerLassoEntity.active(source)==null,"The pickable physical hook can be cut");
|
||||
rope=SmokerLassoEntity.launch(source,p,null);catchTarget(h,rope);source.discard();tick(h,rope);
|
||||
h.assertTrue(rope.isRemoved()&&!SmokerLassoEntity.pulled(p),"A removed source never leaves a captive or a saved cable");h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void onlyOneSourceCableAndOneCapturePerVictim(GameTestHelper h) {
|
||||
var at=arena(h,7);var source=smoker(h,at);var second=smoker(h,at.offset(2,0,0));var p=rider(h,at.offset(0,0,10));var rope=SmokerLassoEntity.launch(source,p,null);
|
||||
h.assertTrue(SmokerLassoEntity.launch(source,p,null)==null,"A source cannot stack flying lassos");catchTarget(h,rope);
|
||||
h.assertTrue(SmokerLassoEntity.launch(second,p,null)==null,"A captured player cannot be reeled by another source simultaneously");rope.discard();source.discard();second.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void teleportsAndCreativeModeBreakEstablishedPulls(GameTestHelper h) {
|
||||
var at=arena(h,8);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));var rope=SmokerLassoEntity.launch(source,p,null);catchTarget(h,rope);p.teleportTo(p.getX()+5,p.getY(),p.getZ());tick(h,rope);
|
||||
h.assertTrue(rope.isRemoved()&&!SmokerLassoEntity.pulled(p),"An external player teleport is not turned into a remote swept pull");
|
||||
var creative=Progression003GameTests.player(h,GameType.CREATIVE);creative.setPos(Vec3.atBottomCenterOf(at.offset(0,0,10)));
|
||||
h.assertTrue(SmokerLassoEntity.launch(source,creative,null)==null,"Creative players cannot be captured");
|
||||
p.setPos(Vec3.atBottomCenterOf(at.offset(0,0,10)));rope=SmokerLassoEntity.launch(source,p,null);catchTarget(h,rope);source.setPos(source.position().add(2,0,0));tick(h,rope);
|
||||
h.assertTrue(rope.isRemoved()&&!SmokerLassoEntity.pulled(p),"Moving the anchor cuts the cable instead of dragging across a teleport");source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void hostileCaptureCannotBeReplacedByAnOrdinaryZipline(GameTestHelper h) {
|
||||
var at=arena(h,9);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));var rope=SmokerLassoEntity.launch(source,p,null);catchTarget(h,rope);p.setShiftKeyDown(true);
|
||||
tick(h,rope);h.assertTrue(SmokerLassoEntity.pulled(p),"Sneaking does not cancel a hostile capture after a real hit: removed="+rope.isRemoved()+" reeling="+rope.isReeling()+" position="+p.position()+" at="+at);
|
||||
p.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND,new ItemStack(Ziplines.GRAPPLING_ROD));p.setItemInHand(net.minecraft.world.InteractionHand.OFF_HAND,new ItemStack(Items.LEAD));
|
||||
h.assertTrue(!PortableZipline.launch(p,net.minecraft.world.InteractionHand.MAIN_HAND,p.getMainHandItem(),1)&&p.getOffhandItem().getCount()==1,"A second movement session is refused without spending a lead");
|
||||
h.assertTrue(PulleyHeadController.startRide(h.getLevel(),p,at)==net.minecraft.world.InteractionResult.CONSUME&&!PulleyHeadController.hasRide(p),"Fixed pulleys also refuse a competing ride");rope.discard();source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void cooldownPersistsButNoRopeOrBabyIsRestored(GameTestHelper h) {
|
||||
var at=arena(h,10);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));source.setNoAi(false);source.setTarget(p);for(int i=0;i<8;i++)tick(h,source);
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());source.save(output);var tag=output.buildResult();tag.putBoolean("IsBaby",true);
|
||||
var loaded=EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),tag),h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof SmokerEntity s&&!s.isBaby()&&!s.windingUp()&&s.lassoCooldown()==160&&SmokerLassoEntity.active(s)==null,"Reload keeps cooldown, rejects baby data and cannot replay preparation or a rope");
|
||||
source.setNoAi(true);var rope=SmokerLassoEntity.launch(source,p,null);var out=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());
|
||||
h.assertTrue(!rope.save(out),"A lasso and its trip are never serialized as world hazards");rope.discard();source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void unloadedCorridorIsRejectedWithoutLoadingIt(GameTestHelper h) {
|
||||
var at=arena(h,11);var source=smoker(h,at);var p=rider(h,at.offset(0,0,10));var rope=SmokerLassoEntity.launch(source,p,null);var remote=h.absolutePos(new BlockPos(10000,180,10000));
|
||||
h.assertTrue(!h.getLevel().hasChunkAt(remote),"Remote chunk begins unloaded");rope.setPos(Vec3.atCenterOf(remote));tick(h,rope);
|
||||
h.assertTrue(rope.isRemoved()&&!h.getLevel().hasChunkAt(remote),"A remote projectile is dropped without requesting its chunk");source.discard();h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarGNeedsAHitThenPullsTheActualEnemy(GameTestHelper h) {
|
||||
var p=pet(h,12);var pet=FamiliarService.active(p);var enemy=enemy(h,pet.position().add(0,0,9));FamiliarBattle.engage(p,enemy,false);
|
||||
var activation=FamiliarBattle.activate(p);h.assertTrue(activation.equals("activated"),"Native manual activation starts the familiar's G preparation: "+activation+" active="+FamiliarBattle.active(p)+" pet="+pet.position()+" owner="+p.position()+" enemy="+enemy.position()+" sight="+pet.hasLineOfSight(enemy));var cast=FamiliarBattle.state(p).cast;for(int i=0;i<22;i++)FamiliarTechniques.advance(pet);
|
||||
var rope=SmokerLassoEntity.active(pet);h.assertTrue(rope!=null&&enemy.getHealth()==20&&!SmokerLassoEntity.pulled(enemy),"Release alone neither hits nor captures the enemy");
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now()&&CompanionService.record(p).readyAt()==FamiliarBattle.bond(p).readyAt(),"G pays the durable shared cooldown on release");
|
||||
catchTarget(h,rope);h.assertTrue(rope.isReeling()&&enemy.getHealth()<20&&cast.hit.contains(enemy.getUUID()),"Actual projectile contact passes through shared familiar damage and control rules");
|
||||
h.assertTrue(FamiliarTechniques.begin(pet,FamiliarBattle.profile(p).initiative(),true).equals("preparing"),"Automatic fire cannot replace an active lasso");ride(h,rope);
|
||||
h.assertTrue(enemy.distanceTo(pet)<2&&pet.isAlive()&&!pet.babyAppearance(),"The enemy travels physically to the adult miniature familiar");enemy.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarMissKeepsCooldownAndFollowCancelsTheCable(GameTestHelper h) {
|
||||
var p=pet(h,13);var pet=FamiliarService.active(p);var enemy=enemy(h,pet.position().add(0,0,9));FamiliarBattle.engage(p,enemy,false);var activation=FamiliarBattle.activate(p);h.assertTrue(activation.equals("activated"),"Miss fixture activates G: "+activation);for(int i=0;i<22;i++)FamiliarTechniques.advance(pet);
|
||||
var rope=SmokerLassoEntity.active(pet);tick(h,rope);enemy.setPos(enemy.position().add(3,0,0));ride(h,rope);
|
||||
h.assertTrue(enemy.getHealth()==20&&!SmokerLassoEntity.pulled(enemy)&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"A dodged G cannot become a guaranteed grab or refund its cooldown");
|
||||
var other=enemy(h,pet.position().add(0,0,9));var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,other,other.position());rope=SmokerLassoEntity.launch(pet,other,cast);catchTarget(h,rope);
|
||||
h.assertTrue(rope.isReeling(),"A separate real hit establishes the follow-cancellation case");FamiliarBattle.cancel(p);
|
||||
h.assertTrue(rope.isRemoved()&&!SmokerLassoEntity.pulled(other)&&FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"The ordinary cancel/follow path releases the victim while retaining spent cooldown");enemy.discard();other.discard();cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarCannotLassoOwnerPassiveAnimalsOrBosses(GameTestHelper h) {
|
||||
var p=pet(h,14);var pet=FamiliarService.active(p);var enemy=enemy(h,pet.position().add(0,0,9));p.setPos(pet.position().add(0,0,4));h.getLevel().addNewPlayer(p);
|
||||
var cow=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);cow.setNoAi(true);cow.setPos(pet.position().add(0,0,6));h.getLevel().addFreshEntity(cow);
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());var rope=SmokerLassoEntity.launch(pet,enemy,cast);catchTarget(h,rope);
|
||||
h.assertTrue(rope.isReeling()&&rope.caughtEntity()==enemy&&!SmokerLassoEntity.pulled(p)&&!SmokerLassoEntity.pulled(cow)&&cow.getHealth()==10,"A physical lasso ignores the owner and passive bystanders without dragging or hurting them: removed="+rope.isRemoved()+" reeling="+rope.isReeling()+" caught="+rope.caughtEntity()+" ownerPulled="+SmokerLassoEntity.pulled(p)+" cow="+cow.getHealth()+" target="+enemy.getHealth()+" hook="+rope.position());rope.discard();
|
||||
var wither=EntityTypes.WITHER.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
h.assertTrue(SmokerLassoEntity.launch(pet,wither,cast)==null,"Bosses cannot be captured even through a server-created cast");
|
||||
enemy.discard();cow.discard();cleanup(p);h.getLevel().removePlayerImmediately(p,Entity.RemovalReason.DISCARDED);h.succeed();
|
||||
}
|
||||
@GameTest public void miniatureSizesRetainAdultGeometryAndIdentity(GameTestHelper h) {
|
||||
var p=pet(h,15);var id=FamiliarBattle.bond(p).id();
|
||||
for(int value:new int[]{120,500,1000,1500,3000,6000}) {
|
||||
var egg=FamiliarBattle.egg(p).copy();var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount+=10;FamiliarService.tick(p);
|
||||
var pet=FamiliarService.active(p);h.assertTrue(pet!=null&&!pet.babyAppearance()&&Math.abs(pet.visualHeight()-.85*value/1000)<.001&&FamiliarBattle.bond(p).id().equals(id),"Every supported size keeps the adult miniature and bond: "+value);
|
||||
}
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarStopsBeforeEnteringAProtectedCell(GameTestHelper h) {
|
||||
var p=pet(h,16);var pet=FamiliarService.active(p);var enemy=enemy(h,pet.position().add(0,0,9));
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,enemy,enemy.position());var rope=SmokerLassoEntity.launch(pet,enemy,cast);catchTarget(h,rope);
|
||||
h.assertTrue(rope.isReeling(),"A permitted physical hit first establishes the cable");var before=enemy.position();
|
||||
try {
|
||||
DENIED.set(BlockPos.containing(before.add(0,0,-ZiplineMotion.SPEED)));tick(h,rope);
|
||||
h.assertTrue(rope.isRemoved()&&!SmokerLassoEntity.pulled(enemy)&&enemy.position().equals(before),"A claim veto stops the next step before the enemy enters its protected cell");
|
||||
} finally { DENIED.set(null);rope.discard();enemy.discard();cleanup(p); }
|
||||
h.succeed();
|
||||
}
|
||||
private static ServerPlayer pet(GameTestHelper h,int slot) {
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIE);p.setPos(Vec3.atBottomCenterOf(arena(h,slot)));var egg=new ItemStack(Infected.SMOKER_EGG);var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount=200;FamiliarService.tick(p);
|
||||
var pet=FamiliarService.active(p);pet.setNoAi(true);pet.setPos(p.position());pet.setOnGround(true);return p;
|
||||
}
|
||||
private static net.minecraft.world.entity.monster.zombie.Zombie enemy(GameTestHelper h,Vec3 at) { var mob=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setNoAi(true);mob.setPos(at);h.getLevel().addFreshEntity(mob);return mob; }
|
||||
private static void cleanup(ServerPlayer p) { AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);CompanionService.clear(p); }
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.familiar.*;
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Grounded native potion clouds, actual projectiles and shared familiar damage protections. */
|
||||
public final class Spitter283GameTests {
|
||||
private static BlockPos arena(GameTestHelper h,int slot) {
|
||||
var at=h.absolutePos(new BlockPos(0,80+slot*8,0));
|
||||
for(int x=-4;x<=4;x++)for(int z=-3;z<=12;z++) {
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,-1,z),Blocks.STONE.defaultBlockState());
|
||||
h.getLevel().setBlockAndUpdate(at.offset(x,4,z),Blocks.STONE.defaultBlockState());
|
||||
}
|
||||
return at;
|
||||
}
|
||||
private static SpitterEntity source(GameTestHelper h,BlockPos at) {
|
||||
var source=Infected.SPITTER.create(h.getLevel(),EntitySpawnReason.COMMAND);source.setNoAi(true);
|
||||
source.setPos(Vec3.atBottomCenterOf(at));h.getLevel().addFreshEntity(source);return source;
|
||||
}
|
||||
private static Zombie enemy(GameTestHelper h,Vec3 at) {
|
||||
var target=EntityTypes.ZOMBIE.create(h.getLevel(),EntitySpawnReason.COMMAND);target.setNoAi(true);
|
||||
target.setPos(at);h.getLevel().addFreshEntity(target);return target;
|
||||
}
|
||||
private static BlockHitResult ground(Vec3 at) { return new BlockHitResult(at,Direction.UP,BlockPos.containing(at).below(),false); }
|
||||
private static void tick(GameTestHelper h,Entity entity,int count,LivingEntity... victims) {
|
||||
for(int i=0;i<count&&!entity.isRemoved();i++) {
|
||||
for(var target:victims)h.getLevel().tickNonPassenger(target);
|
||||
h.getLevel().tickNonPassenger(entity);
|
||||
}
|
||||
}
|
||||
@GameTest public void eggProfileAndAdultRule(GameTestHelper h) {
|
||||
var egg=new ItemStack(Infected.SPITTER_EGG);var mob=Infected.SPITTER.create(h.getLevel(),EntitySpawnReason.COMMAND);mob.setBaby(true);
|
||||
h.assertTrue(SpawnEggItem.getType(egg)==Infected.SPITTER&&SpawnEggItem.byId(Infected.SPITTER).orElseThrow().value()==Infected.SPITTER_EGG,"Spitter uses a dedicated native spawn egg");
|
||||
h.assertTrue(!mob.isBaby()&&mob.getMaxHealth()==10&&mob.getAttributeValue(Attributes.MOVEMENT_SPEED)==.24,"Spitter always has adult geometry and its own attributes");
|
||||
var profile=FamiliarCatalog.bundled().forEgg(egg);
|
||||
h.assertTrue(profile.id().equals("sanctuary:spitter")&&profile.health()==12&&profile.signature().effect().equals("acid_zone"),"Bundled familiar has the ranged acid-zone profile");
|
||||
var attribute=net.minecraft.world.attribute.EnvironmentAttributes.NATURAL_MOB_SPAWNS;
|
||||
for(var b:h.getLevel().getChunkSource().getGenerator().getBiomeSource().possibleBiomes()) {
|
||||
var entries=b.value().getAttributes().applyModifier(attribute,attribute.defaultValue()).getMobsToSpawn(MobCategory.MONSTER).unwrap();
|
||||
if(entries.stream().noneMatch(e->e.value().type()==EntityTypes.ZOMBIE))continue;
|
||||
var found=entries.stream().filter(e->e.value().type()==Infected.SPITTER).toList();
|
||||
h.assertTrue(found.size()==1&&found.getFirst().weight()==4&&found.getFirst().value().count().minInclusive()==1&&found.getFirst().value().count().maxInclusive()==1,"Zombie habitats receive a rare singleton Spitter");
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void waitingPulseExitAndExpiryHaveNoLingeringStatus(GameTestHelper h) {
|
||||
var at=arena(h,1);var source=source(h,at);var target=enemy(h,Vec3.atBottomCenterOf(at.offset(0,0,5)));
|
||||
var zone=AcidZoneEntity.create(source,ground(target.position()),null);
|
||||
h.assertTrue(zone!=null&&zone instanceof AreaEffectCloud&&target.getHealth()==20,"Impact creates a native potion-cloud zone without direct damage");
|
||||
tick(h,zone,9,target);h.assertTrue(target.getHealth()==20,"The first half-second allows escape without damage");
|
||||
tick(h,zone,32,target);h.assertTrue(target.getHealth()==18&&target.getActiveEffects().isEmpty(),"Two one-second pulses hurt without poison or slowness: "+target.getHealth());
|
||||
target.setPos(target.position().add(4,0,0));tick(h,zone,40,target);
|
||||
h.assertTrue(target.getHealth()==18&&target.getActiveEffects().isEmpty(),"Leaving immediately stops all subsequent zone damage");
|
||||
tick(h,zone,40,target);h.assertTrue(zone.isRemoved()&&AcidZoneEntity.active(source)==null,"The finite shrinking zone expires and clears its source reference");
|
||||
h.assertTrue(h.getLevel().getBlockState(at.offset(0,-1,5)).is(Blocks.STONE),"Acid does not alter the floor");source.discard();target.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void wallsFloorsAndInfectedAreProtected(GameTestHelper h) {
|
||||
var at=arena(h,2);var source=source(h,at);var center=Vec3.atBottomCenterOf(at.offset(0,0,5));
|
||||
var hit=enemy(h,center.add(-.5,0,0));var hidden=enemy(h,center.add(1.8,0,0));var upstairs=enemy(h,center.add(0,1,1));
|
||||
var relative=source(h,at.offset(0,0,5));
|
||||
for(int z=4;z<=6;z++)for(int y=0;y<3;y++)h.getLevel().setBlockAndUpdate(at.offset(1,y,z),Blocks.STONE.defaultBlockState());
|
||||
h.getLevel().setBlockAndUpdate(at.offset(0,0,6),Blocks.STONE.defaultBlockState());
|
||||
var zone=AcidZoneEntity.create(source,ground(center),null);tick(h,zone,41,hit,hidden,upstairs);
|
||||
h.assertTrue(hit.getHealth()==18&&hidden.getHealth()==20&&upstairs.getHealth()==20,"A wall and a higher floor prevent cloud damage to separated entities");
|
||||
h.assertTrue(source.getHealth()==10&&relative.getHealth()==10,"The source and infected family resist wild acid");
|
||||
zone.discard();source.discard();relative.discard();hit.discard();hidden.discard();upstairs.discard();h.succeed();
|
||||
}
|
||||
@GameTest public void oneCloudPerSourceAndNoSavedHazards(GameTestHelper h) {
|
||||
var at=arena(h,3);var source=source(h,at);var a=AcidZoneEntity.create(source,ground(Vec3.atBottomCenterOf(at.offset(0,0,4))),null);
|
||||
var b=AcidZoneEntity.create(source,ground(Vec3.atBottomCenterOf(at.offset(0,0,6))),null);
|
||||
h.assertTrue(a.isRemoved()&&!b.isRemoved()&&AcidZoneEntity.active(source)==b,"A second impact replaces rather than stacks the old zone");
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());
|
||||
var shot=new AcidSpitEntity(h.getLevel(),source);
|
||||
h.assertTrue(!b.save(output)&&!shot.save(output),"Neither transient acid clouds nor projectiles enter world saves");
|
||||
source.discard();tick(h,b,1);h.assertTrue(b.isRemoved(),"Removing the source removes its hazard on the next tick");h.succeed();
|
||||
}
|
||||
@GameTest public void unsupportedAndUnloadedImpactsCreateNothing(GameTestHelper h) {
|
||||
var at=arena(h,4);var source=source(h,at);var air=Vec3.atBottomCenterOf(at.offset(0,3,6));
|
||||
h.assertTrue(AcidZoneEntity.create(source,ground(air),null)==null,"An impact too far above ground cannot float a cloud in midair");
|
||||
var remote=h.absolutePos(new BlockPos(10000,90,10000));
|
||||
h.assertTrue(!h.getLevel().hasChunkAt(remote)&&AcidZoneEntity.create(source,ground(Vec3.atCenterOf(remote)),null)==null
|
||||
&&!h.getLevel().hasChunkAt(remote),"An unloaded impact is rejected without loading or generating a chunk");
|
||||
var shot=new AcidSpitEntity(h.getLevel(),source);shot.setPos(Vec3.atCenterOf(remote));h.getLevel().tickNonPassenger(shot);
|
||||
h.assertTrue(shot.isRemoved()&&!h.getLevel().hasChunkAt(remote),"Projectile guards also reject an unloaded movement corridor");source.discard();h.succeed();
|
||||
}
|
||||
@GameTest(maxTicks=95) public void wildWindupAndNativeProjectileCreateGroundZone(GameTestHelper h) {
|
||||
var at=arena(h,5);var source=source(h,at);source.setNoAi(false);source.setOnGround(true);
|
||||
var target=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);target.setNoAi(true);target.setPos(Vec3.atBottomCenterOf(at.offset(0,0,7)));
|
||||
h.getLevel().addFreshEntity(target);source.setTarget(target);
|
||||
h.runAfterDelay(8,()->h.assertTrue(source.windingUp()&&AcidZoneEntity.active(source)==null,"Ranged attack visibly prepares before any hazard exists"));
|
||||
h.runAfterDelay(45,()->{
|
||||
var zone=AcidZoneEntity.active(source);
|
||||
h.assertTrue(zone!=null&&zone.position().distanceTo(target.position())<1&&target.getHealth()<10,"Real native projectile contact deposits a grounded damaging potion-style zone");
|
||||
h.assertTrue(source.spitCooldown()>0&&target.getActiveEffects().isEmpty(),"Cooldown prevents continuous fire and the victim receives no lingering debuff");
|
||||
zone.discard();source.discard();target.discard();h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest(maxTicks=40) public void interruptedWindupReloadsAsCooldownAndCannotShootThroughWall(GameTestHelper h) {
|
||||
var at=arena(h,6);var source=source(h,at);source.setNoAi(false);source.setOnGround(true);
|
||||
var target=enemy(h,Vec3.atBottomCenterOf(at.offset(0,0,7)));source.setTarget(target);
|
||||
h.runAfterDelay(8,()->{
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());source.save(output);
|
||||
var tag=output.buildResult();tag.putBoolean("IsBaby",true);
|
||||
var loaded=EntityType.loadEntityRecursive(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),tag),h.getLevel(),EntitySpawnReason.LOAD,e->e);
|
||||
h.assertTrue(loaded instanceof SpitterEntity s&&!s.isBaby()&&!s.windingUp()&&s.spitCooldown()==100,"Reload keeps adult-only behavior and cooldown without replaying windup");
|
||||
for(int x=-4;x<=4;x++)for(int y=0;y<4;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,3),Blocks.STONE.defaultBlockState());
|
||||
});
|
||||
h.runAfterDelay(30,()->{
|
||||
h.assertTrue(!source.windingUp()&&AcidZoneEntity.active(source)==null&&target.getHealth()==20,"Losing line of sight cancels preparation without a through-wall projectile");
|
||||
source.discard();target.discard();h.succeed();
|
||||
});
|
||||
}
|
||||
@GameTest public void familiarProjectileZoneRepeatsDamageAndProtectsOwnerAndPassiveAnimals(GameTestHelper h) {
|
||||
var p=pet(h,7);var pet=FamiliarService.active(p);var target=enemy(h,p.position().add(0,0,5));
|
||||
var cast=new FamiliarTechniques.Cast(pet,FamiliarBattle.profile(p).signature(),false,target,target.position());
|
||||
FamiliarBattle.state(p).cast=cast;
|
||||
for(int i=0;i<25;i++)FamiliarTechniques.advance(pet);
|
||||
h.assertTrue(FamiliarBattle.bond(p).readyAt()>FamiliarBattle.now(),"The normal release commits durable shared cooldown");
|
||||
var shots=h.getLevel().getEntitiesOfClass(AcidSpitEntity.class,pet.getBoundingBox().inflate(10));
|
||||
h.assertTrue(shots.size()==1,"The familiar releases one real acid projectile");
|
||||
h.assertTrue(FamiliarTechniques.begin(pet,FamiliarBattle.profile(p).initiative(),true).equals("preparing"),"Automatic fire waits while the released G projectile is still in flight");
|
||||
tick(h,shots.getFirst(),20);
|
||||
var zone=AcidZoneEntity.active(pet);h.assertTrue(zone!=null&&target.getHealth()==20,"Actual familiar impact creates the cloud without instant hit damage");
|
||||
h.assertTrue(FamiliarTechniques.begin(pet,FamiliarBattle.profile(p).initiative(),true).equals("preparing")&&AcidZoneEntity.active(pet)==zone,"Automatic fire preserves the active signature zone instead of replacing it with a small pool");
|
||||
p.setPos(target.position());var animals=new java.util.ArrayList<LivingEntity>();
|
||||
for(int i=0;i<13;i++) {
|
||||
var animal=EntityTypes.COW.create(h.getLevel(),EntitySpawnReason.COMMAND);animal.setNoAi(true);animal.setPos(target.position());h.getLevel().addFreshEntity(animal);animals.add(animal);
|
||||
}
|
||||
var victims=new java.util.ArrayList<>(animals);victims.add(target);
|
||||
float health=p.getHealth();tick(h,zone,41,victims.toArray(LivingEntity[]::new));
|
||||
h.assertTrue(target.getHealth()<19&&cast.hit.contains(target.getUUID()),"The same enemy can receive repeated pulses through shared familiar damage rules");
|
||||
h.assertTrue(p.getHealth()==health&&animals.stream().allMatch(e->e.getHealth()==10)&&target.getActiveEffects().isEmpty(),"Owner and passive crowds remain protected and cannot mask enemies from the bounded pulse");
|
||||
cleanup(p);tick(h,zone,1);h.assertTrue(zone.isRemoved(),"Unequipping the familiar cancels its remaining zone");target.discard();animals.forEach(Entity::discard);h.succeed();
|
||||
}
|
||||
@GameTest public void familiarSizesUseAdultProportionsAndPersistOnEgg(GameTestHelper h) {
|
||||
var p=pet(h,8);var egg=FamiliarBattle.egg(p).copy();var original=FamiliarBattle.bond(p).id();
|
||||
for(int value:new int[]{500,1000,1500}) {
|
||||
var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg.copy());p.tickCount+=10;FamiliarService.tick(p);var pet=FamiliarService.active(p);
|
||||
h.assertTrue(!pet.babyAppearance()&&FamiliarSize.value(pet.egg())==value&&Math.abs(pet.visualHeight()-.85F*value/1000)<.001,"The variable miniature scales an adult body: "+value);
|
||||
h.assertTrue(FamiliarBattle.bond(p).id().equals(original),"Changing size preserves the familiar bond identifier");
|
||||
}
|
||||
cleanup(p);h.succeed();
|
||||
}
|
||||
private static ServerPlayer pet(GameTestHelper h,int slot) {
|
||||
var p=Companion019GameTests.player(h,CompanionType.ZOMBIE);var at=arena(h,slot);p.setPos(Vec3.atBottomCenterOf(at));
|
||||
var egg=new ItemStack(Infected.SPITTER_EGG);var size=new net.minecraft.nbt.CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);
|
||||
net.minecraft.world.item.component.CustomData.update(net.minecraft.core.component.DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.tickCount=200;FamiliarService.tick(p);
|
||||
var pet=FamiliarService.active(p);pet.setPos(p.position());pet.setOnGround(true);pet.setNoAi(true);return p;
|
||||
}
|
||||
private static void cleanup(ServerPlayer p) { AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);FamiliarBattle.release(p);CompanionService.clear(p); }
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"entrypoints": {
|
||||
"client": ["fr.koka.sanctuary.gametest.Starter039TestSupport"],
|
||||
"preLaunch": ["fr.koka.sanctuary.gametest.Inventory010MixinAudit"],
|
||||
"fabric-gametest": ["fr.koka.sanctuary.gametest.Menu275GameTests", "fr.koka.sanctuary.gametest.ClaimMap273GameTests", "fr.koka.sanctuary.gametest.ChunkClaims272GameTests", "fr.koka.sanctuary.gametest.Inhabitant271GameTests","fr.koka.sanctuary.gametest.Property270GameTests","fr.koka.sanctuary.gametest.Providence269GameTests","fr.koka.sanctuary.gametest.Providence268GameTests","fr.koka.sanctuary.gametest.Providence265GameTests","fr.koka.sanctuary.gametest.Insect245GameTests","fr.koka.sanctuary.gametest.Forgerie238GameTests","fr.koka.sanctuary.gametest.Aptitudes236GameTests","fr.koka.sanctuary.gametest.Diagonal231GameTests", "fr.koka.sanctuary.gametest.Diagonal230GameTests", "fr.koka.sanctuary.gametest.Diagonal229GameTests", "fr.koka.sanctuary.gametest.Diagonal228GameTests", "fr.koka.sanctuary.gametest.Diagonal227GameTests", "fr.koka.sanctuary.gametest.Diagonal226GameTests", "fr.koka.sanctuary.gametest.Diagonal225GameTests", "fr.koka.sanctuary.gametest.East224GameTests", "fr.koka.sanctuary.gametest.East223GameTests", "fr.koka.sanctuary.gametest.YellowRock222GameTests", "fr.koka.sanctuary.gametest.West221GameTests", "fr.koka.sanctuary.gametest.West220GameTests", "fr.koka.sanctuary.gametest.West219GameTests", "fr.koka.sanctuary.gametest.West218GameTests", "fr.koka.sanctuary.gametest.West217GameTests", "fr.koka.sanctuary.gametest.Base204GameTests", "fr.koka.sanctuary.gametest.North205GameTests", "fr.koka.sanctuary.gametest.Palace173GameTests", "fr.koka.sanctuary.gametest.Plans072GameTests", "fr.koka.sanctuary.gametest.Starter052GameTests", "fr.koka.sanctuary.gametest.Dispenser051GameTests", "fr.koka.sanctuary.gametest.Head050GameTests", "fr.koka.sanctuary.gametest.Poultry048GameTests", "fr.koka.sanctuary.gametest.StarInspection047GameTests", "fr.koka.sanctuary.gametest.SkyFlight046GameTests", "fr.koka.sanctuary.gametest.GravesFood038GameTests","fr.koka.sanctuary.gametest.Realtime037GameTests","fr.koka.sanctuary.gametest.Corrections036GameTests","fr.koka.sanctuary.gametest.Collections035GameTests","fr.koka.sanctuary.gametest.Notifications034GameTests","fr.koka.sanctuary.gametest.Menus033GameTests","${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.Drained307GameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests", "fr.koka.sanctuary.gametest.AncientExpeditions001GameTests", "fr.koka.sanctuary.gametest.Progression003GameTests", "fr.koka.sanctuary.gametest.Atlas005GameTests", "fr.koka.sanctuary.gametest.Demeure006GameTests", "fr.koka.sanctuary.gametest.Mining008GameTests", "fr.koka.sanctuary.gametest.Building009GameTests", "fr.koka.sanctuary.gametest.Inventory010GameTests", "fr.koka.sanctuary.gametest.Cycle011GameTests", "fr.koka.sanctuary.gametest.Inventory012GameTests", "fr.koka.sanctuary.gametest.Inventory014GameTests", "fr.koka.sanctuary.gametest.Tutorial016GameTests", "fr.koka.sanctuary.gametest.Movement017GameTests", "fr.koka.sanctuary.gametest.Accessory018GameTests", "fr.koka.sanctuary.gametest.Companion019GameTests", "fr.koka.sanctuary.gametest.Companion032GameTests", "fr.koka.sanctuary.gametest.Recipes024GameTests", "fr.koka.sanctuary.gametest.Carry027GameTests", "fr.koka.sanctuary.gametest.Familiar029GameTests", "fr.koka.sanctuary.gametest.Dynamic030GameTests", "fr.koka.sanctuary.gametest.GraveInventory039GameTests", "fr.koka.sanctuary.gametest.FoodKnowledge039GameTests", "fr.koka.sanctuary.gametest.OperatorStars040GameTests", "fr.koka.sanctuary.gametest.Starter039GameTests", "fr.koka.sanctuary.gametest.Horde167GameTests", "fr.koka.sanctuary.gametest.Horde168GameTests", "fr.koka.sanctuary.gametest.Horde169GameTests", "fr.koka.sanctuary.gametest.Horde170GameTests", "fr.koka.sanctuary.gametest.Horde171GameTests", "fr.koka.sanctuary.gametest.Horde172GameTests", "fr.koka.sanctuary.gametest.Waystone239GameTests"],
|
||||
"fabric-gametest": ["fr.koka.sanctuary.gametest.HappyCreeperGameTests", "fr.koka.sanctuary.gametest.FlyingWhale290GameTests", "fr.koka.sanctuary.gametest.Hunter286GameTests", "fr.koka.sanctuary.gametest.Smoker285GameTests", "fr.koka.sanctuary.gametest.Boomer284GameTests", "fr.koka.sanctuary.gametest.Spitter283GameTests", "fr.koka.sanctuary.gametest.Charger282GameTests", "fr.koka.sanctuary.gametest.Infected280GameTests", "fr.koka.sanctuary.gametest.Menu275GameTests", "fr.koka.sanctuary.gametest.ClaimMap273GameTests", "fr.koka.sanctuary.gametest.ChunkClaims272GameTests", "fr.koka.sanctuary.gametest.Inhabitant271GameTests","fr.koka.sanctuary.gametest.Property270GameTests","fr.koka.sanctuary.gametest.Providence269GameTests","fr.koka.sanctuary.gametest.Providence268GameTests","fr.koka.sanctuary.gametest.Providence265GameTests","fr.koka.sanctuary.gametest.Insect245GameTests","fr.koka.sanctuary.gametest.Forgerie238GameTests","fr.koka.sanctuary.gametest.Aptitudes236GameTests","fr.koka.sanctuary.gametest.Diagonal231GameTests", "fr.koka.sanctuary.gametest.Diagonal230GameTests", "fr.koka.sanctuary.gametest.Diagonal229GameTests", "fr.koka.sanctuary.gametest.Diagonal228GameTests", "fr.koka.sanctuary.gametest.Diagonal227GameTests", "fr.koka.sanctuary.gametest.Diagonal226GameTests", "fr.koka.sanctuary.gametest.Diagonal225GameTests", "fr.koka.sanctuary.gametest.East224GameTests", "fr.koka.sanctuary.gametest.East223GameTests", "fr.koka.sanctuary.gametest.YellowRock222GameTests", "fr.koka.sanctuary.gametest.West221GameTests", "fr.koka.sanctuary.gametest.West220GameTests", "fr.koka.sanctuary.gametest.West219GameTests", "fr.koka.sanctuary.gametest.West218GameTests", "fr.koka.sanctuary.gametest.West217GameTests", "fr.koka.sanctuary.gametest.Base204GameTests", "fr.koka.sanctuary.gametest.North205GameTests", "fr.koka.sanctuary.gametest.Palace173GameTests", "fr.koka.sanctuary.gametest.Plans072GameTests", "fr.koka.sanctuary.gametest.Starter052GameTests", "fr.koka.sanctuary.gametest.Dispenser051GameTests", "fr.koka.sanctuary.gametest.Head050GameTests", "fr.koka.sanctuary.gametest.Poultry048GameTests", "fr.koka.sanctuary.gametest.StarInspection047GameTests", "fr.koka.sanctuary.gametest.SkyFlight046GameTests", "fr.koka.sanctuary.gametest.GravesFood038GameTests","fr.koka.sanctuary.gametest.Realtime037GameTests","fr.koka.sanctuary.gametest.Corrections036GameTests","fr.koka.sanctuary.gametest.Collections035GameTests","fr.koka.sanctuary.gametest.Notifications034GameTests","fr.koka.sanctuary.gametest.Menus033GameTests","${gametest_entrypoint}", "fr.koka.sanctuary.gametest.Swamp27WorldGameTests", "fr.koka.sanctuary.gametest.Caves28WorldGameTests", "fr.koka.sanctuary.gametest.Terraces29WorldGameTests", "fr.koka.sanctuary.gametest.River30WorldGameTests", "fr.koka.sanctuary.gametest.Transit29WorldGameTests", "fr.koka.sanctuary.gametest.Alpha28NativeStructuresGameTests", "fr.koka.sanctuary.gametest.Village306GameTests", "fr.koka.sanctuary.gametest.Drained307GameTests", "fr.koka.sanctuary.gametest.BlockKnowledgeGameTests", "fr.koka.sanctuary.gametest.CensusGameTestsTerrain", "fr.koka.sanctuary.gametest.CensusStockGameTests", "fr.koka.sanctuary.gametest.MaterialActivityGameTests", "fr.koka.sanctuary.gametest.AncientExpeditions001GameTests", "fr.koka.sanctuary.gametest.Progression003GameTests", "fr.koka.sanctuary.gametest.Atlas005GameTests", "fr.koka.sanctuary.gametest.Demeure006GameTests", "fr.koka.sanctuary.gametest.Mining008GameTests", "fr.koka.sanctuary.gametest.Building009GameTests", "fr.koka.sanctuary.gametest.Inventory010GameTests", "fr.koka.sanctuary.gametest.Cycle011GameTests", "fr.koka.sanctuary.gametest.Inventory012GameTests", "fr.koka.sanctuary.gametest.Inventory014GameTests", "fr.koka.sanctuary.gametest.Tutorial016GameTests", "fr.koka.sanctuary.gametest.Movement017GameTests", "fr.koka.sanctuary.gametest.Accessory018GameTests", "fr.koka.sanctuary.gametest.Companion019GameTests", "fr.koka.sanctuary.gametest.Companion032GameTests", "fr.koka.sanctuary.gametest.Recipes024GameTests", "fr.koka.sanctuary.gametest.Carry027GameTests", "fr.koka.sanctuary.gametest.Familiar029GameTests", "fr.koka.sanctuary.gametest.Dynamic030GameTests", "fr.koka.sanctuary.gametest.GraveInventory039GameTests", "fr.koka.sanctuary.gametest.FoodKnowledge039GameTests", "fr.koka.sanctuary.gametest.OperatorStars040GameTests", "fr.koka.sanctuary.gametest.Starter039GameTests", "fr.koka.sanctuary.gametest.Horde167GameTests", "fr.koka.sanctuary.gametest.Horde168GameTests", "fr.koka.sanctuary.gametest.Horde169GameTests", "fr.koka.sanctuary.gametest.Horde170GameTests", "fr.koka.sanctuary.gametest.Horde171GameTests", "fr.koka.sanctuary.gametest.Horde172GameTests", "fr.koka.sanctuary.gametest.Waystone239GameTests"],
|
||||
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
|
||||
},
|
||||
"mixins": ["sanctuary-gametest.mixins.json"],
|
||||
|
||||
@@ -45,6 +45,8 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
public void onInitialize() {
|
||||
fr.koka.sanctuary.property.PropertyService.register();
|
||||
fr.koka.sanctuary.insects.Insects.register();
|
||||
fr.koka.sanctuary.infected.Infected.register();
|
||||
fr.koka.sanctuary.whale.FlyingWhales.register();
|
||||
fr.koka.sanctuary.waystone.Waystones.register();
|
||||
fr.koka.sanctuary.zipline.Ziplines.register();
|
||||
fr.koka.sanctuary.palace.OriginBlock.register();
|
||||
|
||||
@@ -9,7 +9,7 @@ public record AtlasMarker(String id,int x,Integer y,int z,String kind,String spr
|
||||
|| kind.equals("banner")&&y==null || name==null || name.length()>96) throw new IllegalArgumentException("Atlas marker");
|
||||
}
|
||||
public boolean inView(AtlasPayloads.Request request) {
|
||||
long minX=((long)request.centerX()-AtlasPayloads.SIDE/2)*16,minZ=((long)request.centerZ()-AtlasPayloads.SIDE/2)*16;
|
||||
return x>=minX&&z>=minZ&&x<minX+AtlasPayloads.PIXELS&&z<minZ+AtlasPayloads.PIXELS&&(y==null||y<=request.altitude());
|
||||
long minX=request.minBlockX(),minZ=request.minBlockZ();
|
||||
return x>=minX&&z>=minZ&&x<minX+request.blockSpan()&&z<minZ+request.blockSpan()&&(y==null||y<=request.altitude());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,21 @@ import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
public final class AtlasPayloads {
|
||||
public static final int SIDE=64, PIXELS=SIDE*16, BATCH=64;
|
||||
private AtlasPayloads() { }
|
||||
public record Request(int token,int centerX,int centerZ,boolean operator,int altitude) implements CustomPacketPayload {
|
||||
public record Request(int token,int centerX,int centerZ,boolean operator,int altitude,int detail) implements CustomPacketPayload {
|
||||
public static final Type<Request> TYPE=new Type<>(SanctuaryMod.id("atlas_request"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,Request> CODEC=CustomPacketPayload.codec(
|
||||
(p,b) -> {b.writeInt(p.token);b.writeInt(p.centerX);b.writeInt(p.centerZ);b.writeBoolean(p.operator);b.writeVarInt(p.altitude);},
|
||||
b -> new Request(b.readInt(),b.readInt(),b.readInt(),b.readBoolean(),b.readVarInt()));
|
||||
(p,b) -> {b.writeInt(p.token);b.writeInt(p.centerX);b.writeInt(p.centerZ);b.writeBoolean(p.operator);b.writeVarInt(p.altitude);b.writeVarInt(p.detail);},
|
||||
b -> new Request(b.readInt(),b.readInt(),b.readInt(),b.readBoolean(),b.readVarInt(),b.readVarInt()));
|
||||
public Request(int token,int centerX,int centerZ,boolean operator) {this(token,centerX,centerZ,operator,AtlasAltitude.SURFACE);}
|
||||
public Request { new AtlasStore.Key(centerX,centerZ);AtlasAltitude.validate(altitude); }
|
||||
public Request(int token,int centerX,int centerZ,boolean operator,int altitude) {this(token,centerX,centerZ,operator,altitude,0);}
|
||||
public Request { new AtlasStore.Key(centerX,centerZ);AtlasAltitude.validate(altitude);if(detail<0 || detail>AtlasPyramid.MAX_LEVEL)throw new IllegalArgumentException("Atlas detail level"); }
|
||||
public int step() {return 1<<detail;}
|
||||
public int tileCenterX() {return Math.floorDiv(centerX,step());}
|
||||
public int tileCenterZ() {return Math.floorDiv(centerZ,step());}
|
||||
public long minBlockX() {return ((long)tileCenterX()-SIDE/2)*16*step();}
|
||||
public long minBlockZ() {return ((long)tileCenterZ()-SIDE/2)*16*step();}
|
||||
public long blockSpan() {return (long)PIXELS*step();}
|
||||
public boolean containsTile(int x,int z) {return x>=tileCenterX()-SIDE/2 && x<tileCenterX()+SIDE/2 && z>=tileCenterZ()-SIDE/2 && z<tileCenterZ()+SIDE/2;}
|
||||
public Type<Request> type() { return TYPE; }
|
||||
}
|
||||
public record Response(int token,boolean reset,boolean done,boolean canOperate,boolean operator,
|
||||
|
||||
@@ -32,10 +32,10 @@ public record AtlasPlayersPayload(int token,List<Player> players) implements Cus
|
||||
public Type<AtlasPlayersPayload> type(){return TYPE;}
|
||||
public static AtlasPlayersPayload snapshot(ServerPlayer viewer,AtlasPayloads.Request request) {
|
||||
if(!AtlasService.canRead(viewer,request.operator()))return new AtlasPlayersPayload(request.token(),List.of());
|
||||
double minX=((long)request.centerX()-AtlasPayloads.SIDE/2)*16,minZ=((long)request.centerZ()-AtlasPayloads.SIDE/2)*16;
|
||||
double minX=request.minBlockX(),minZ=request.minBlockZ();
|
||||
var players=viewer.level().getServer().getPlayerList().getPlayers().stream()
|
||||
.filter(p->p.level()==viewer.level() && p.isAlive() && (!p.isSpectator() || p==viewer))
|
||||
.filter(p->p.getX()>=minX && p.getZ()>=minZ && p.getX()<minX+AtlasPayloads.PIXELS && p.getZ()<minZ+AtlasPayloads.PIXELS)
|
||||
.filter(p->p.getX()>=minX && p.getZ()>=minZ && p.getX()<minX+request.blockSpan() && p.getZ()<minZ+request.blockSpan())
|
||||
.sorted(Comparator.comparingDouble(p->p.distanceToSqr(viewer))).limit(LIMIT)
|
||||
.map(p->new Player(p.getUUID(),p.getGameProfile().name(),p.getX(),p.getZ())).toList();
|
||||
return new AtlasPlayersPayload(request.token(),players);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package fr.koka.sanctuary.atlas;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/** Disposable mip levels of one authorized survey. Never persisted or filled from unknown terrain. */
|
||||
public final class AtlasPyramid {
|
||||
public static final int MAX_LEVEL=16;
|
||||
private final List<Map<AtlasStore.Key,byte[]>> levels=new ArrayList<>();
|
||||
private final List<Set<AtlasStore.Key>> dirty=new ArrayList<>();
|
||||
|
||||
public AtlasPyramid(Map<AtlasStore.Key,byte[]> source) {
|
||||
levels.add(source);dirty.add(new HashSet<>());
|
||||
}
|
||||
public void changed(AtlasStore.Key key) {
|
||||
for(int level=1;level<levels.size();level++) {
|
||||
key=parent(key);dirty.get(level).add(key);
|
||||
}
|
||||
}
|
||||
Map<AtlasStore.Key,byte[]> view(int level) {
|
||||
if(level<0 || level>MAX_LEVEL)throw new IllegalArgumentException("Atlas detail level");
|
||||
for(int i=1;i<=level;i++) {
|
||||
if(i==levels.size()) {
|
||||
levels.add(new HashMap<>());dirty.add(new HashSet<>());
|
||||
for(var key:levels.get(i-1).keySet())dirty.get(i).add(parent(key));
|
||||
}
|
||||
var target=levels.get(i);
|
||||
for(var key:dirty.get(i))target.put(key,reduce(levels.get(i-1),key));
|
||||
dirty.get(i).clear();
|
||||
}
|
||||
return Collections.unmodifiableMap(levels.get(level));
|
||||
}
|
||||
private static AtlasStore.Key parent(AtlasStore.Key key) {
|
||||
return new AtlasStore.Key(Math.floorDiv(key.x(),2),Math.floorDiv(key.z(),2));
|
||||
}
|
||||
private static byte[] reduce(Map<AtlasStore.Key,byte[]> source,AtlasStore.Key key) {
|
||||
byte[] out=new byte[256];
|
||||
for(int dz=0;dz<2;dz++)for(int dx=0;dx<2;dx++) {
|
||||
int cx=key.x()*2+dx,cz=key.z()*2+dz;
|
||||
if(Math.abs((long)cx)>1875000 || Math.abs((long)cz)>1875000)continue;
|
||||
var child=source.get(new AtlasStore.Key(cx,cz));
|
||||
if(child==null)continue;
|
||||
for(int z=0;z<8;z++)for(int x=0;x<8;x++) {
|
||||
int p=z*32+x*2;
|
||||
out[(dz*8+z)*16+dx*8+x]=representative(child[p],child[p+1],child[p+16],child[p+17]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
/** Prefer known terrain over surveyed void, then the most common packed map colour. */
|
||||
private static byte representative(byte a,byte b,byte c,byte d) {
|
||||
byte best=a;int score=score(a,a,b,c,d),next=score(b,a,b,c,d);
|
||||
if(next>score){best=b;score=next;}next=score(c,a,b,c,d);
|
||||
if(next>score){best=c;score=next;}next=score(d,a,b,c,d);
|
||||
if(next>score)best=d;
|
||||
return best;
|
||||
}
|
||||
private static int score(byte value,byte a,byte b,byte c,byte d) {
|
||||
if(value==0)return -1;
|
||||
return (Byte.toUnsignedInt(value)>=4?4:0)+(value==a?1:0)+(value==b?1:0)+(value==c?1:0)+(value==d?1:0);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ public final class AtlasService {
|
||||
int lastSnapshot;
|
||||
final ArrayDeque<AtlasStore.Key> revisits=new ArrayDeque<>();
|
||||
final Map<Integer,AtlasStore> layers=new LinkedHashMap<>();
|
||||
final Map<AtlasStore.Key,byte[]> inspection=new HashMap<>();
|
||||
AtlasPyramid inspectionPyramid=new AtlasPyramid(inspection);
|
||||
}
|
||||
private record Job(AtlasPayloads.Request request,ArrayDeque<AtlasStore.Key> remaining) { }
|
||||
private AtlasService() { }
|
||||
@@ -87,7 +89,7 @@ public final class AtlasService {
|
||||
try {for (var store:state.layers.values()) store.save();}
|
||||
catch (Exception error) {state.failed=true;unsubscribe(state);SanctuaryMod.LOGGER.error("Could not save atlas (file preserved)",error);}
|
||||
}
|
||||
private static void unsubscribe(State state) {state.job=null;state.viewport=null;state.updates.clear();}
|
||||
private static void unsubscribe(State state) {state.job=null;state.viewport=null;state.updates.clear();state.inspection.clear();state.inspectionPyramid=new AtlasPyramid(state.inspection);}
|
||||
private static boolean canObserve(ServerPlayer player) {
|
||||
var session=ProgressionService.session(player.level().getServer());
|
||||
return session!=null && session.healthy && ProgressEffects.get(player)!=null
|
||||
@@ -134,9 +136,10 @@ public final class AtlasService {
|
||||
for (var entry:samples.entrySet()) {
|
||||
if (!state.layers.get(entry.getKey()).observe(entry.getValue())) continue;
|
||||
var viewport=state.viewport;
|
||||
if (viewport!=null && !viewport.operator() && viewport.altitude()==entry.getKey()
|
||||
&& inside(pos.x(),pos.z(),viewport.centerX()-AtlasPayloads.SIDE/2,viewport.centerZ()-AtlasPayloads.SIDE/2))
|
||||
state.updates.add(new AtlasStore.Key(pos.x(),pos.z()));
|
||||
if (viewport!=null && !viewport.operator() && viewport.altitude()==entry.getKey()) {
|
||||
int x=Math.floorDiv(pos.x(),viewport.step()),z=Math.floorDiv(pos.z(),viewport.step());
|
||||
if(viewport.containsTile(x,z))state.updates.add(new AtlasStore.Key(x,z));
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void request(ServerPlayer player,AtlasPayloads.Request request) {
|
||||
@@ -144,6 +147,7 @@ public final class AtlasService {
|
||||
if (session==null || !session.healthy) {reply(player,request,true,true,"unavailable",List.of());return;}
|
||||
var state=state(player);
|
||||
if (request.token()==0) {unsubscribe(state);fr.koka.sanctuary.property.ChunkPropertyService.closeOverlay(player);return;}
|
||||
if(request.detail()>0)fr.koka.sanctuary.property.ChunkPropertyService.closeOverlay(player);
|
||||
int tick=player.level().getServer().getTickCount();
|
||||
unsubscribe(state);
|
||||
long now=System.nanoTime();
|
||||
@@ -155,24 +159,21 @@ public final class AtlasService {
|
||||
}
|
||||
var survey=state.layers.get(request.altitude());
|
||||
var keys=new ArrayList<AtlasStore.Key>();
|
||||
int minX=request.centerX()-AtlasPayloads.SIDE/2,minZ=request.centerZ()-AtlasPayloads.SIDE/2;
|
||||
if (request.operator()) {
|
||||
for (var chunk:LoadedChunks.full(player.level())) {
|
||||
var p=chunk.getPos();
|
||||
if (inside(p.x(),p.z(),minX,minZ)) keys.add(new AtlasStore.Key(p.x(),p.z()));
|
||||
if (request.containsTile(Math.floorDiv(p.x(),request.step()),Math.floorDiv(p.z(),request.step()))) keys.add(new AtlasStore.Key(p.x(),p.z()));
|
||||
}
|
||||
} else {
|
||||
for (int z=minZ;z<minZ+AtlasPayloads.SIDE;z++) for (int x=minX;x<minX+AtlasPayloads.SIDE;x++) {
|
||||
if (Math.abs((long)x)<=1875000 && Math.abs((long)z)<=1875000 && survey.contains(x,z)) keys.add(new AtlasStore.Key(x,z));
|
||||
}
|
||||
for(var key:survey.view(request.detail()).keySet())if(request.containsTile(key.x(),key.z()))keys.add(key);
|
||||
}
|
||||
keys.sort(Comparator.comparingLong(k -> Math.abs((long)k.x()-request.centerX())+Math.abs((long)k.z()-request.centerZ())));
|
||||
int cx=request.operator()?request.centerX():request.tileCenterX(),cz=request.operator()?request.centerZ():request.tileCenterZ();
|
||||
keys.sort(Comparator.comparingLong(k -> Math.abs((long)k.x()-cx)+Math.abs((long)k.z()-cz)));
|
||||
state.viewport=request;
|
||||
reply(player,request,true,keys.isEmpty(),survey.full() ? "full" : "ready",List.of());
|
||||
state.lastSnapshot=tick;
|
||||
if (!keys.isEmpty()) state.job=new Job(request,new ArrayDeque<>(keys));
|
||||
}
|
||||
private static boolean inside(int x,int z,int minX,int minZ) {return x>=minX && x<minX+AtlasPayloads.SIDE && z>=minZ && z<minZ+AtlasPayloads.SIDE;}
|
||||
private static void tick(MinecraftServer server) {
|
||||
var session=ProgressionService.session(server);
|
||||
if (session==null || !session.healthy) return;
|
||||
@@ -239,16 +240,25 @@ public final class AtlasService {
|
||||
while (budget>0 && updates.hasNext()) {
|
||||
var key=updates.next();updates.remove();budget--;
|
||||
sent.add(key);
|
||||
var tile=state.layers.get(request.altitude()).get(key.x(),key.z());
|
||||
var tile=state.layers.get(request.altitude()).get(key.x(),key.z(),request.detail());
|
||||
if (tile!=null) tiles.add(tile);
|
||||
}
|
||||
while (budget-->0 && job!=null && !job.remaining.isEmpty()) {
|
||||
var key=job.remaining.removeFirst();
|
||||
if (!sent.add(key)) {budget++;continue;}
|
||||
var tile=request.operator() ? AtlasSampler.sample(player.level(),key.x(),key.z(),request.altitude())
|
||||
: state.layers.get(request.altitude()).get(key.x(),key.z());
|
||||
: state.layers.get(request.altitude()).get(key.x(),key.z(),request.detail());
|
||||
if (tile!=null) tiles.add(tile);
|
||||
}
|
||||
if(request.operator() && request.detail()>0 && !tiles.isEmpty()) {
|
||||
var changed=new LinkedHashSet<AtlasStore.Key>();
|
||||
for(var tile:tiles) {
|
||||
var key=new AtlasStore.Key(tile.x(),tile.z());state.inspection.put(key,tile.colors());state.inspectionPyramid.changed(key);
|
||||
changed.add(new AtlasStore.Key(Math.floorDiv(key.x(),request.step()),Math.floorDiv(key.z(),request.step())));
|
||||
}
|
||||
var view=state.inspectionPyramid.view(request.detail());tiles.clear();
|
||||
for(var key:changed)tiles.add(new AtlasStore.Tile(key.x(),key.z(),view.get(key)));
|
||||
}
|
||||
boolean done=job==null || job.remaining.isEmpty();
|
||||
if (job!=null || !tiles.isEmpty()) reply(player,request,false,done,state.layers.get(request.altitude()).full() ? "full" : "ready",tiles);
|
||||
if (done) state.job=null;
|
||||
@@ -259,8 +269,8 @@ public final class AtlasService {
|
||||
if (ServerPlayNetworking.canSend(player,AtlasPayloads.Response.TYPE)) ServerPlayNetworking.send(player,
|
||||
new AtlasPayloads.Response(request.token(),reset,done,canOperate(player),request.operator(),status,tiles));
|
||||
if (reset && (status.equals("ready") || status.equals("full"))) snapshot(player,request);
|
||||
if (!tiles.isEmpty()) fr.koka.sanctuary.property.ChunkPropertyService.mapTiles(player,request.token(),tiles);
|
||||
if (!tiles.isEmpty() && ServerPlayNetworking.canSend(player,AtlasDemeurePayload.TYPE)) ServerPlayNetworking.send(player,
|
||||
if (request.detail()==0 && !tiles.isEmpty()) fr.koka.sanctuary.property.ChunkPropertyService.mapTiles(player,request.token(),tiles);
|
||||
if (request.detail()==0 && !tiles.isEmpty() && ServerPlayNetworking.canSend(player,AtlasDemeurePayload.TYPE)) ServerPlayNetworking.send(player,
|
||||
new AtlasDemeurePayload(request.token(),AtlasDemeurePayload.forTiles(player,request.operator(),tiles)));
|
||||
}
|
||||
private static void snapshot(ServerPlayer player,AtlasPayloads.Request request) {
|
||||
|
||||
@@ -10,8 +10,8 @@ import java.util.*;
|
||||
|
||||
/** One inhabitant's Overworld observations. Never populated from another player's atlas. */
|
||||
public final class AtlasStore {
|
||||
public static final int MAX_TILES=16384;
|
||||
private static final int MAX_BYTES=12*1024*1024;
|
||||
public static final int MAX_TILES=131072;
|
||||
private static final int MAX_BYTES=64*1024*1024;
|
||||
public record Key(int x,int z) {
|
||||
public Key { if (Math.abs((long)x)>1875000 || Math.abs((long)z)>1875000) throw new IllegalArgumentException("Atlas coordinates"); }
|
||||
}
|
||||
@@ -23,6 +23,7 @@ public final class AtlasStore {
|
||||
private final UUID owner;
|
||||
private final long seed;
|
||||
private final Map<Key,byte[]> tiles=new LinkedHashMap<>();
|
||||
private final AtlasPyramid pyramid=new AtlasPyramid(tiles);
|
||||
private byte[] previous;
|
||||
private boolean dirty;
|
||||
|
||||
@@ -53,6 +54,8 @@ public final class AtlasStore {
|
||||
public boolean dirty() { return dirty; }
|
||||
public boolean contains(int x,int z) { return tiles.containsKey(new Key(x,z)); }
|
||||
public Tile get(int x,int z) { var data=tiles.get(new Key(x,z));return data==null ? null : new Tile(x,z,data); }
|
||||
Map<Key,byte[]> view(int detail) {return pyramid.view(detail);}
|
||||
public Tile get(int x,int z,int detail) {var data=view(detail).get(new Key(x,z));return data==null?null:new Tile(x,z,data);}
|
||||
public boolean observe(Tile tile) {
|
||||
var key=new Key(tile.x(),tile.z());
|
||||
if (!tiles.containsKey(key) && full()) return false;
|
||||
@@ -63,7 +66,7 @@ public final class AtlasStore {
|
||||
known|=colors[i]!=0;
|
||||
}
|
||||
if (!known || Arrays.equals(old,colors)) return false;
|
||||
tiles.put(key,colors);dirty=true;return true;
|
||||
tiles.put(key,colors);pyramid.changed(key);dirty=true;return true;
|
||||
}
|
||||
public void save() throws IOException {
|
||||
if (!dirty) return;
|
||||
|
||||
@@ -28,7 +28,9 @@ public final class CompanionTooltips {
|
||||
}
|
||||
public static void register(){
|
||||
ItemTooltipCallback.EVENT.register((stack,context,flags,lines)->{
|
||||
var type=type(stack);if(type==null||!ProgressionClient.active())return;
|
||||
var type=type(stack);
|
||||
var profile=fr.koka.sanctuary.familiar.FamiliarCatalog.bundled().forEgg(stack);
|
||||
if((type==null&&profile==null)||!ProgressionClient.active())return;
|
||||
boolean details=(CompanionClient.studied||ProgressionClient.operator);
|
||||
var extra=new java.util.ArrayList<Component>();
|
||||
extra.add(rarity(stack).label().copy().withStyle(color(stack)));
|
||||
@@ -47,7 +49,6 @@ public final class CompanionTooltips {
|
||||
extra.add(Component.translatable("sanctuary.familiar.nautilus.mount.hint").withStyle(ChatFormatting.AQUA));
|
||||
if(fr.koka.sanctuary.cosmetics.FamiliarMountTraits.walksOnLava(stack)&&fr.koka.sanctuary.cosmetics.FamiliarSize.colossal(stack))
|
||||
extra.add(Component.translatable("sanctuary.familiar.strider.mount.hint").withStyle(ChatFormatting.GOLD));
|
||||
var profile=fr.koka.sanctuary.familiar.FamiliarCatalog.bundled().forEgg(stack);
|
||||
if(profile!=null){
|
||||
if(FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals(profile.id()))profile=FamiliarClient.profile;
|
||||
var initiative=profile.initiative();
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.NbtIo;
|
||||
|
||||
/** One-time client default; servers.dat remains personal and outside the pack index. */
|
||||
public final class DefaultServerList {
|
||||
public static final String ADDRESS = "beta.sanctuary-minecraft.net";
|
||||
public static final String ICON = "/assets/sanctuary/server-icon.png";
|
||||
private DefaultServerList() {}
|
||||
|
||||
public static void install(Path gameDirectory) throws IOException {
|
||||
Path marker = gameDirectory.resolve(".sanctuary/default-server-beta");
|
||||
if (Files.exists(marker)) return;
|
||||
Path file = gameDirectory.resolve("servers.dat");
|
||||
CompoundTag root = NbtIo.read(file);
|
||||
if (root == null) root = new CompoundTag();
|
||||
if (root.contains("servers") && !(root.get("servers") instanceof ListTag)) {
|
||||
throw new IOException("Unexpected server list format; preserving servers.dat");
|
||||
}
|
||||
ListTag servers = root.getListOrEmpty("servers");
|
||||
CompoundTag existing = null;
|
||||
for (var tag : servers) {
|
||||
if (!(tag instanceof CompoundTag server)) {
|
||||
throw new IOException("Unexpected server entry; preserving servers.dat");
|
||||
}
|
||||
if (isSanctuary(server.getStringOr("ip", ""))) existing = server;
|
||||
}
|
||||
boolean changed = existing == null || existing.getBooleanOr("hidden", false);
|
||||
if (existing == null) {
|
||||
CompoundTag server = new CompoundTag();
|
||||
server.putString("name", "Sanctuary Beta");
|
||||
server.putString("ip", ADDRESS);
|
||||
try (var icon = DefaultServerList.class.getResourceAsStream(ICON)) {
|
||||
if (icon == null) throw new IOException("Missing Sanctuary server icon");
|
||||
server.putString("icon", Base64.getEncoder().encodeToString(icon.readAllBytes()));
|
||||
}
|
||||
servers.add(server);
|
||||
} else if (changed) {
|
||||
existing.putBoolean("hidden", false);
|
||||
}
|
||||
if (changed) {
|
||||
root.put("servers", servers);
|
||||
Path backup = gameDirectory.resolve("servers.dat.before-sanctuary-beta");
|
||||
if (Files.exists(file) && !Files.exists(backup)) Files.copy(file, backup);
|
||||
Path temporary = Files.createTempFile(gameDirectory, "sanctuary-servers-", ".dat");
|
||||
try {
|
||||
NbtIo.write(root, temporary);
|
||||
try {
|
||||
Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (AtomicMoveNotSupportedException ignored) {
|
||||
Files.move(temporary, file, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(temporary);
|
||||
}
|
||||
}
|
||||
Files.createDirectories(marker.getParent());
|
||||
Files.writeString(marker, ADDRESS + "\n");
|
||||
}
|
||||
|
||||
private static boolean isSanctuary(String address) {
|
||||
String normalized = address.strip().toLowerCase(Locale.ROOT);
|
||||
int separator = normalized.indexOf(':');
|
||||
String host = separator < 0 ? normalized : normalized.substring(0, separator);
|
||||
String port = separator < 0 ? "" : normalized.substring(separator);
|
||||
if (host.endsWith(".")) host = host.substring(0, host.length() - 1);
|
||||
return host.equals(ADDRESS) && (port.isEmpty() || port.equals(":25565") || port.equals(":25567"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.lighting.DynamicLightField;
|
||||
|
||||
/** Render-thread capture handed to a terrain worker with its copied block data. */
|
||||
public interface DynamicLightContext extends DynamicLightRegion {
|
||||
void sanctuary$captureLights(DynamicLightField.Snapshot snapshot);
|
||||
}
|
||||
@@ -56,6 +56,7 @@ public final class FamiliarRenderer extends EntityRenderer<FamiliarEntity,Famili
|
||||
mob.xOld=pet.xOld;mob.yOld=pet.yOld;mob.zOld=pet.zOld;
|
||||
mob.xo=pet.xo;mob.yo=pet.yo;mob.zo=pet.zo;
|
||||
mob.yRotO=pet.yRotO;mob.xRotO=pet.xRotO;
|
||||
if(mob instanceof fr.koka.sanctuary.infected.HunterEntity)mob.setPose(pet.getPose());
|
||||
mob.setOnGround(pet.onGround());mob.setDeltaMovement(pet.getDeltaMovement());
|
||||
mob.yBodyRot=pet.yBodyRot;mob.yBodyRotO=pet.yBodyRotO;mob.yHeadRot=pet.yHeadRot;mob.yHeadRotO=pet.yHeadRotO;
|
||||
mob.tickCount=pet.tickCount;
|
||||
@@ -64,6 +65,10 @@ public final class FamiliarRenderer extends EntityRenderer<FamiliarEntity,Famili
|
||||
mob.setAttached(fr.koka.sanctuary.cosmetics.AccessoryService.APPEARANCE,pet.getAttachedOrElse(fr.koka.sanctuary.cosmetics.AccessoryService.APPEARANCE,fr.koka.sanctuary.cosmetics.AccessoryService.Appearance.EMPTY));
|
||||
var renderer=entityRenderDispatcher.getRenderer(mob);
|
||||
var appearance=renderer.createRenderState(mob,partialTick);
|
||||
if(appearance instanceof InfectedRenderer.State zombie) {
|
||||
var identity=fr.koka.sanctuary.familiar.FamiliarIdentity.get(pet.egg());
|
||||
zombie.skin=ZombieSkins.index(identity==null?pet.getUUID():identity);
|
||||
}
|
||||
// Render the real companion's name separately, at its miniature scale.
|
||||
appearance.nameTag=null;
|
||||
// Giant native models have an eye probe above the actual miniature. Use the real
|
||||
|
||||
@@ -44,7 +44,7 @@ public final class FamiliarScreen extends BlocodexFrame {
|
||||
boolean workMode=FamiliarClient.view!=null&&FamiliarClient.view.work();
|
||||
var selected=bond.variant()?profile.variant():profile.signature();
|
||||
var type=CompanionTooltips.type(egg);
|
||||
Component workName=CompanionClient.label(type.id()+".active_name");
|
||||
Component workName=type==null?FamiliarBattle.label("no_work"):CompanionClient.label(type.id()+".active_name");
|
||||
layout.addChild(new MultiLineTextWidget(FamiliarBattle.label("selected_power",workMode?workName:FamiliarClient.name(selected)),font).setMaxWidth(width));
|
||||
layout.addChild(new StringWidget(FamiliarBattle.label("technique"),font));
|
||||
var technique=layout.addChild(LinearLayout.horizontal().spacing(4));
|
||||
@@ -52,14 +52,16 @@ public final class FamiliarScreen extends BlocodexFrame {
|
||||
var signature=technique.addChild(Button.builder(FamiliarClient.name(profile.signature()),b->FamiliarClient.action("signature")).width(half).build());signature.active=!combat&&(workMode||bond.variant());
|
||||
var variant=technique.addChild(Button.builder(FamiliarClient.name(profile.variant()),b->FamiliarClient.action("variant")).width(half).build());
|
||||
variant.active=!combat&&(workMode||!bond.variant())&&bond.learned()&&CompanionClient.record!=null&&CompanionClient.record.unlocked();
|
||||
var work=layout.addChild(Button.builder(FamiliarBattle.label(workMode?"work_selected":"work"),b->FamiliarClient.action("work")).width(width).build());
|
||||
boolean unlocked=ProgressionClient.operator||(CompanionClient.record!=null&&CompanionClient.record.unlocked());
|
||||
work.active=!combat&&!workMode&&unlocked;
|
||||
work.setTooltip(Tooltip.create(FamiliarBattle.label(unlocked?"work_help":"work_locked",CompanionClient.controls())));
|
||||
if(!unlocked)layout.addChild(new MultiLineTextWidget(FamiliarBattle.label("work_locked"),font).setMaxWidth(width));
|
||||
if(type!=null){
|
||||
var work=layout.addChild(Button.builder(FamiliarBattle.label(workMode?"work_selected":"work"),b->FamiliarClient.action("work")).width(width).build());
|
||||
work.active=!combat&&!workMode&&unlocked;
|
||||
work.setTooltip(Tooltip.create(FamiliarBattle.label(unlocked?"work_help":"work_locked",CompanionClient.controls())));
|
||||
if(!unlocked)layout.addChild(new MultiLineTextWidget(FamiliarBattle.label("work_locked"),font).setMaxWidth(width));
|
||||
}
|
||||
if(workMode){
|
||||
layout.addChild(new MultiLineTextWidget(FamiliarBattle.label("work_help",CompanionClient.controls()),font).setMaxWidth(width));
|
||||
if(CompanionClient.studied)layout.addChild(new MultiLineTextWidget(CompanionClient.label(type.id()+".active"),font).setMaxWidth(width));
|
||||
if(type!=null&&CompanionClient.studied)layout.addChild(new MultiLineTextWidget(CompanionClient.label(type.id()+".active"),font).setMaxWidth(width));
|
||||
}else{
|
||||
layout.addChild(new MultiLineTextWidget(FamiliarClient.help(selected),font).setMaxWidth(width));
|
||||
layout.addChild(new MultiLineTextWidget(FamiliarBattle.label("technique_stats",selected.windup()/20F,selected.cooldown()/20F),font).setMaxWidth(width));
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Ported from Sanctuary 26.2 itsalive; see docs/flying-whale-beta290.md.
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.client.FlyingWhaleRenderState;
|
||||
import net.minecraft.client.model.EntityModel;
|
||||
import net.minecraft.client.model.geom.ModelLayerLocation;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.model.geom.PartPose;
|
||||
import net.minecraft.client.model.geom.builders.CubeDeformation;
|
||||
import net.minecraft.client.model.geom.builders.CubeListBuilder;
|
||||
import net.minecraft.client.model.geom.builders.LayerDefinition;
|
||||
import net.minecraft.client.model.geom.builders.MeshDefinition;
|
||||
import net.minecraft.client.model.geom.builders.PartDefinition;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
public final class FlyingWhaleModel
|
||||
extends EntityModel<FlyingWhaleRenderState> {
|
||||
public static final ModelLayerLocation LAYER_LOCATION = new ModelLayerLocation(SanctuaryMod.id("flying_whale"), "main");
|
||||
private static final float DEG_TO_RAD = (float)Math.PI / 180;
|
||||
private final ModelPart whale;
|
||||
private final ModelPart body;
|
||||
private final ModelPart head;
|
||||
private final ModelPart jaw;
|
||||
private final ModelPart leftFin;
|
||||
private final ModelPart rightFin;
|
||||
private final ModelPart dorsalFin;
|
||||
private final ModelPart tailBase;
|
||||
private final ModelPart tailMid;
|
||||
private final ModelPart tailFluke;
|
||||
|
||||
public FlyingWhaleModel(ModelPart root) {
|
||||
super(root, RenderTypes::entityTranslucent);
|
||||
this.whale = root.getChild("whale");
|
||||
this.body = this.whale.getChild("body");
|
||||
this.head = this.whale.getChild("head");
|
||||
this.jaw = this.head.getChild("jaw");
|
||||
this.leftFin = this.whale.getChild("left_fin");
|
||||
this.rightFin = this.whale.getChild("right_fin");
|
||||
this.dorsalFin = this.whale.getChild("dorsal_fin");
|
||||
this.tailBase = this.whale.getChild("tail_base");
|
||||
this.tailMid = this.tailBase.getChild("tail_mid");
|
||||
this.tailFluke = this.tailMid.getChild("tail_fluke");
|
||||
}
|
||||
|
||||
public static LayerDefinition createBodyLayer() {
|
||||
MeshDefinition mesh = new MeshDefinition();
|
||||
PartDefinition root = mesh.getRoot();
|
||||
PartDefinition whale = root.addOrReplaceChild("whale", CubeListBuilder.create(), PartPose.offset(0.0f, 5.0f, 0.0f));
|
||||
whale.addOrReplaceChild("body", CubeListBuilder.create().texOffs(0, 0).addBox(-22.0f, -18.0f, -46.0f, 44.0f, 32.0f, 92.0f, new CubeDeformation(0.35f)).texOffs(0, 126).addBox(-17.0f, 4.0f, -40.0f, 34.0f, 13.0f, 78.0f, new CubeDeformation(0.12f)), PartPose.ZERO);
|
||||
PartDefinition head = whale.addOrReplaceChild("head", CubeListBuilder.create().texOffs(206, 0).addBox(-20.0f, -16.0f, -28.0f, 40.0f, 28.0f, 32.0f, new CubeDeformation(0.2f)).texOffs(206, 64).addBox(-14.0f, -8.0f, -42.0f, 28.0f, 18.0f, 18.0f, new CubeDeformation(0.1f)).texOffs(306, 64).addBox(-4.0f, -19.0f, -17.0f, 8.0f, 5.0f, 9.0f), PartPose.offset(0.0f, 0.0f, -45.0f));
|
||||
head.addOrReplaceChild("jaw", CubeListBuilder.create().texOffs(206, 106).addBox(-12.0f, 0.0f, -21.0f, 24.0f, 8.0f, 24.0f), PartPose.offset(0.0f, 8.0f, -20.0f));
|
||||
whale.addOrReplaceChild("left_fin", CubeListBuilder.create().texOffs(0, 220).addBox(0.0f, -2.0f, -8.0f, 52.0f, 4.0f, 18.0f, new CubeDeformation(0.04f)), PartPose.offsetAndRotation(18.5f, 3.5f, -20.0f, -0.06981317f, -0.17453292f, 0.41887903f));
|
||||
whale.addOrReplaceChild("right_fin", CubeListBuilder.create().texOffs(0, 220).addBox(-52.0f, -2.0f, -8.0f, 52.0f, 4.0f, 18.0f, new CubeDeformation(0.04f)), PartPose.offsetAndRotation(-18.5f, 3.5f, -20.0f, -0.06981317f, 0.17453292f, -0.41887903f));
|
||||
whale.addOrReplaceChild("dorsal_fin", CubeListBuilder.create().texOffs(150, 220).addBox(-4.0f, -24.0f, -5.0f, 8.0f, 24.0f, 14.0f, new CubeDeformation(0.03f)), PartPose.offsetAndRotation(0.0f, -15.0f, 2.0f, -0.12217305f, 0.0f, 0.0f));
|
||||
PartDefinition tailBase = whale.addOrReplaceChild("tail_base", CubeListBuilder.create().texOffs(356, 0).addBox(-12.0f, -10.0f, 0.0f, 24.0f, 20.0f, 38.0f, new CubeDeformation(0.08f)), PartPose.offset(0.0f, -1.0f, 41.0f));
|
||||
PartDefinition tailMid = tailBase.addOrReplaceChild("tail_mid", CubeListBuilder.create().texOffs(356, 64).addBox(-7.0f, -7.0f, 0.0f, 14.0f, 14.0f, 31.0f), PartPose.offset(0.0f, 0.0f, 34.0f));
|
||||
PartDefinition tailFluke = tailMid.addOrReplaceChild("tail_fluke", CubeListBuilder.create(), PartPose.offset(0.0f, 0.0f, 29.0f));
|
||||
tailFluke.addOrReplaceChild("left_lobe", CubeListBuilder.create().texOffs(256, 178).addBox(0.0f, -2.0f, -9.0f, 38.0f, 4.0f, 22.0f, new CubeDeformation(0.05f)), PartPose.offsetAndRotation(0.0f, 0.0f, 0.0f, 0.0f, -0.12217305f, -0.12217305f));
|
||||
tailFluke.addOrReplaceChild("right_lobe", CubeListBuilder.create().texOffs(256, 178).addBox(-38.0f, -2.0f, -9.0f, 38.0f, 4.0f, 22.0f, new CubeDeformation(0.05f)), PartPose.offsetAndRotation(0.0f, 0.0f, 0.0f, 0.0f, 0.12217305f, 0.12217305f));
|
||||
return LayerDefinition.create(mesh, 512, 256);
|
||||
}
|
||||
|
||||
public void setupAnim(FlyingWhaleRenderState state) {
|
||||
super.setupAnim(state);
|
||||
float age = state.ageInTicks;
|
||||
float slowWave = Mth.sin((age * 0.035f));
|
||||
float tailWave = Mth.sin((age * 0.058f));
|
||||
float bodyRoll = Mth.sin((age * 0.017f)) * 0.035f;
|
||||
float finDrift = Mth.sin((age * 0.041f + 1.7f)) * 0.075f;
|
||||
this.whale.y = 5.0f + slowWave * 1.25f;
|
||||
this.whale.zRot = bodyRoll;
|
||||
this.whale.xRot = Mth.sin((age * 0.013f)) * 0.018f;
|
||||
this.body.zRot = bodyRoll * 0.35f;
|
||||
this.head.xRot = -0.02617994f + Mth.sin((age * 0.025f)) * 0.018f;
|
||||
this.jaw.xRot = (float)Math.PI / 90 + Mth.sin((age * 0.031f)) * 0.022f;
|
||||
this.leftFin.zRot = 0.41887903f + finDrift;
|
||||
this.rightFin.zRot = -0.41887903f - finDrift;
|
||||
this.leftFin.xRot = -0.08726646f + slowWave * 0.025f;
|
||||
this.rightFin.xRot = -0.08726646f - slowWave * 0.025f;
|
||||
this.dorsalFin.zRot = -bodyRoll * 0.65f;
|
||||
this.tailBase.yRot = tailWave * 0.13f;
|
||||
this.tailMid.yRot = tailWave * 0.22f;
|
||||
this.tailFluke.yRot = tailWave * 0.31f;
|
||||
this.tailFluke.zRot = Mth.sin((age * 0.029f)) * 0.045f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Ported from Sanctuary 26.2 itsalive; see docs/flying-whale-beta290.md.
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
|
||||
|
||||
public final class FlyingWhaleRenderState
|
||||
extends LivingEntityRenderState {
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Ported from Sanctuary 26.2 itsalive; see docs/flying-whale-beta290.md.
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import fr.koka.sanctuary.client.FlyingWhaleModel;
|
||||
import fr.koka.sanctuary.client.FlyingWhaleRenderState;
|
||||
import fr.koka.sanctuary.whale.FlyingWhaleEntity;
|
||||
import net.minecraft.client.renderer.entity.EntityRendererProvider;
|
||||
import net.minecraft.client.renderer.entity.MobRenderer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
public final class FlyingWhaleRenderer
|
||||
extends MobRenderer<FlyingWhaleEntity, FlyingWhaleRenderState, FlyingWhaleModel> {
|
||||
private static final Identifier TEXTURE = SanctuaryMod.id("textures/entity/flying_whale/flying_whale.png");
|
||||
private static final float MODEL_SCALE = 2.7f;
|
||||
|
||||
public FlyingWhaleRenderer(EntityRendererProvider.Context context) {
|
||||
super(context, new FlyingWhaleModel(context.bakeLayer(FlyingWhaleModel.LAYER_LOCATION)), 4.25f);
|
||||
}
|
||||
|
||||
public Identifier getTextureLocation(FlyingWhaleRenderState state) {
|
||||
return TEXTURE;
|
||||
}
|
||||
|
||||
public FlyingWhaleRenderState createRenderState() {
|
||||
return new FlyingWhaleRenderState();
|
||||
}
|
||||
|
||||
protected void scale(FlyingWhaleRenderState state, PoseStack poseStack) {
|
||||
poseStack.scale(2.7f, 2.7f, 2.7f);
|
||||
}
|
||||
|
||||
protected float getShadowRadius(FlyingWhaleRenderState state) {
|
||||
return 9.5f;
|
||||
}
|
||||
|
||||
protected int getModelTint(FlyingWhaleRenderState state) {
|
||||
return -1191182337;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.infected.*;
|
||||
import net.minecraft.client.model.monster.zombie.*;
|
||||
import net.minecraft.client.model.geom.ModelLayers;
|
||||
import net.minecraft.client.renderer.entity.*;
|
||||
import net.minecraft.client.renderer.entity.state.ZombieRenderState;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.monster.zombie.Zombie;
|
||||
|
||||
/** Shared random clothes, native zombie arms and the Hunter's crouched ground posture. */
|
||||
public final class InfectedRenderer extends AbstractZombieRenderer<Zombie,InfectedRenderer.State,ZombieModel<InfectedRenderer.State>> {
|
||||
public static final Identifier TEXTURE=ZombieSkins.texture(0,false);
|
||||
public static final class State extends ZombieRenderState {
|
||||
public int skin;
|
||||
}
|
||||
public InfectedRenderer(EntityRendererProvider.Context context) {
|
||||
super(context,new ZombieModel<State>(context.bakeLayer(ModelLayers.ZOMBIE)),
|
||||
new BabyZombieModel<State>(context.bakeLayer(ModelLayers.ZOMBIE_BABY)),
|
||||
ArmorModelSet.bake(ModelLayers.ZOMBIE_ARMOR,context.getModelSet(),ZombieModel<State>::new),
|
||||
ArmorModelSet.bake(ModelLayers.ZOMBIE_BABY_ARMOR,context.getModelSet(),BabyZombieModel<State>::new));
|
||||
}
|
||||
@Override public State createRenderState() { return new State(); }
|
||||
@Override public Identifier getTextureLocation(State state) { return ZombieSkins.texture(state.skin,state.isBaby); }
|
||||
@Override public void extractRenderState(Zombie entity,State state,float partialTick) {
|
||||
super.extractRenderState(entity,state,partialTick);
|
||||
state.skin=ZombieSkins.index(entity.getUUID());
|
||||
// A stationary spawn need not receive a movement packet carrying the grounded bit.
|
||||
if(entity instanceof HunterEntity)state.isCrouching=!entity.isInWater()&&!entity.isPassenger()
|
||||
&&(entity.onGround()||!entity.level().noBlockCollision(entity,entity.getBoundingBox().deflate(.01).move(0,-.06,0)));
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,5 @@ public final class MobHeadState {
|
||||
public HeadCosmeticState head;
|
||||
public float yaw,pitch,scale=1;
|
||||
public boolean active;
|
||||
public boolean happyCreeper;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
private double centerX,centerZ,zoom=2;
|
||||
private int token,originX,originZ,focusX,focusY,focusSize,refresh,tiles;
|
||||
private int altitude=AtlasAltitude.SURFACE;
|
||||
private int detail;
|
||||
private Button altitudeUp,altitudeDown;
|
||||
private boolean operator,canOperate,dragging,dirty,complete,navigable;
|
||||
private boolean grid=false,footprints=false;
|
||||
@@ -95,6 +96,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
centerX=initialTarget.x()+.5;centerZ=initialTarget.z()+.5;request();
|
||||
} else recenter();
|
||||
}
|
||||
else refresh=6;
|
||||
if(propertyOpen)rebuildPropertyMenu();
|
||||
}
|
||||
private void clear() {
|
||||
@@ -128,7 +130,11 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
if (minecraft.player==null || minecraft.level==null) return;
|
||||
centerX=Math.clamp(centerX,-29999488,29999488);centerZ=Math.clamp(centerZ,-29999488,29999488);
|
||||
int chunkX=Math.floorDiv((int)Math.floor(centerX),16),chunkZ=Math.floorDiv((int)Math.floor(centerZ),16);
|
||||
int newX=(chunkX-AtlasPayloads.SIDE/2)*16,newZ=(chunkZ-AtlasPayloads.SIDE/2)*16;
|
||||
int nextDetail=detailForZoom();
|
||||
if(nextDetail!=detail){detail=nextDetail;clear();}
|
||||
int step=1<<detail;
|
||||
int newX=(Math.floorDiv(chunkX,step)-AtlasPayloads.SIDE/2)*16*step;
|
||||
int newZ=(Math.floorDiv(chunkZ,step)-AtlasPayloads.SIDE/2)*16*step;
|
||||
// Reproject retained tiles instead of blanking the window on every drag release.
|
||||
if (newX!=originX || newZ!=originZ) {
|
||||
originX=newX;originZ=newZ;
|
||||
@@ -140,7 +146,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
pendingFootprintBatch=java.util.List.of();
|
||||
token=AtlasClient.nextToken();claimOverlay.begin(token);complete=false;refresh=1600;
|
||||
dimension=minecraft.level.dimension().identifier().toString();
|
||||
AtlasClient.send(new AtlasPayloads.Request(token,chunkX,chunkZ,operator,altitude));
|
||||
AtlasClient.send(new AtlasPayloads.Request(token,chunkX,chunkZ,operator,altitude,detail));
|
||||
}
|
||||
public void accept(AtlasPayloads.Response response) {
|
||||
if (texture==null || response.token()!=token || minecraft.level==null
|
||||
@@ -163,9 +169,9 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
if (!response.tiles().isEmpty()) pendingFootprintBatch=response.tiles().stream()
|
||||
.map(tile -> new fr.koka.sanctuary.atlas.AtlasStore.Key(tile.x(),tile.z())).toList();
|
||||
for (var tile:response.tiles()) {
|
||||
int x=tile.x()*16-originX,z=tile.z()*16-originZ;
|
||||
int x=tile.x()*16-Math.floorDiv(originX,1<<detail),z=tile.z()*16-Math.floorDiv(originZ,1<<detail);
|
||||
if (x<0 || z<0 || x+16>SIZE || z+16>SIZE) continue;
|
||||
claimOverlay.observe(tile.x(),tile.z());
|
||||
if(detail==0)claimOverlay.observe(tile.x(),tile.z());
|
||||
var key=new fr.koka.sanctuary.atlas.AtlasStore.Key(tile.x(),tile.z());
|
||||
byte[] colors=tile.colors(),before=surveyed.put(key,colors);
|
||||
if (!java.util.Arrays.equals(before,colors)) {
|
||||
@@ -181,7 +187,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
if (complete && !wasComplete) refresh=operator ? 100 : 1200;
|
||||
}
|
||||
private void paintTile(fr.koka.sanctuary.atlas.AtlasStore.Key key,byte[] colors) {
|
||||
int x=key.x()*16-originX,z=key.z()*16-originZ;
|
||||
int x=key.x()*16-Math.floorDiv(originX,1<<detail),z=key.z()*16-Math.floorDiv(originZ,1<<detail);
|
||||
if (x<0 || z<0 || x+16>SIZE || z+16>SIZE) return;
|
||||
for (int dz=0;dz<16;dz++) for (int dx=0;dx<16;dx++) {
|
||||
int packed=Byte.toUnsignedInt(colors[dz*16+dx]);
|
||||
@@ -206,9 +212,15 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
}
|
||||
private double mapCenterY() {return focusY+focusSize/2.;}
|
||||
private double clampZoom(double value) {
|
||||
// At the last level the whole world must fit along the shorter window axis too.
|
||||
double minimum=Math.min(width,height)/(SIZE*(double)(1<<fr.koka.sanctuary.atlas.AtlasPyramid.MAX_LEVEL));
|
||||
return Math.clamp(value,minimum,8);
|
||||
}
|
||||
private int detailForZoom() {
|
||||
double extent=Math.max(width,2*Math.max(mapCenterY(),height-mapCenterY()));
|
||||
// Leave room for the viewport origin's chunk alignment (up to 16 blocks).
|
||||
return Math.clamp(value,Math.min(8,Math.max(.75,extent/992.)),8);
|
||||
int level=0;
|
||||
while(level<fr.koka.sanctuary.atlas.AtlasPyramid.MAX_LEVEL && zoom*(1<<level)<extent/992.)level++;
|
||||
return level;
|
||||
}
|
||||
private boolean readable() {return navigable;}
|
||||
private void changeZoom(double next,double x,double y) {
|
||||
@@ -226,7 +238,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
}
|
||||
// Disabled buttons also own their hit area; clicking one must never start a map gesture.
|
||||
if (overControl(event.x(),event.y())) return super.mouseClicked(event,doubleClick);
|
||||
if(readable() && event.button()==InputConstants.MOUSE_BUTTON_RIGHT && inside(event.x(),event.y())){
|
||||
if(detail==0 && detailForZoom()==0 && readable() && event.button()==InputConstants.MOUSE_BUTTON_RIGHT && inside(event.x(),event.y())){
|
||||
int wx=(int)Math.floor(centerX+(event.x()-width/2.)/zoom),wz=(int)Math.floor(centerZ+(event.y()-mapCenterY())/zoom);
|
||||
if(surveyed.containsKey(new fr.koka.sanctuary.atlas.AtlasStore.Key(wx>>4,wz>>4)))openPropertyContext(wx>>4,wz>>4,(int)event.x(),(int)event.y());
|
||||
return true;
|
||||
@@ -272,7 +284,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
graphics.enableScissor(0,0,width,height);
|
||||
if (texture!=null && readable()) {
|
||||
int x=screenX(originX),y=screenZ(originZ);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED,TEXTURE,x,y,0,0,(int)(SIZE*zoom),(int)(SIZE*zoom),SIZE,SIZE,SIZE,SIZE);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED,TEXTURE,x,y,0,0,(int)Math.round(SIZE*(1<<detail)*zoom),(int)Math.round(SIZE*(1<<detail)*zoom),SIZE,SIZE,SIZE,SIZE);
|
||||
renderFootprints(graphics);
|
||||
if (grid) renderGrid(graphics);
|
||||
renderMarkers(graphics);
|
||||
@@ -453,8 +465,9 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
boolean pointer=inside(mouseX,mouseY) && !overControl(mouseX,mouseY) && readable();
|
||||
int wx=(int)Math.floor(centerX+(pointer ? (mouseX-width/2.)/zoom : 0));
|
||||
int wz=(int)Math.floor(centerZ+(pointer ? (mouseY-mapCenterY())/zoom : 0));
|
||||
String coords="X "+wx+" Z "+wz+" · "+String.format(java.util.Locale.ROOT,"%.1f×",zoom);
|
||||
var state=label(operator ? "operator_status" : "inhabitant_status");
|
||||
String scale=zoom>=1?String.format(java.util.Locale.ROOT,"%.1f×",zoom):label("blocks_per_pixel",String.format(java.util.Locale.ROOT,"%,.0f",1/zoom)).getString();
|
||||
String coords="X "+wx+" Z "+wz+" · "+scale;
|
||||
var state=label(detail>0 ? operator ? "operator_overview_status" : "overview_status" : operator ? "operator_status" : "inhabitant_status");
|
||||
int textWidth=Math.min(width-24,Math.max(font.width(coords),font.width(state)));
|
||||
int y=height-58;
|
||||
graphics.fill(5,y-4,17+textWidth,y+24,0xA0101720);
|
||||
@@ -484,6 +497,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
private int screenX(double world) {return (int)Math.round(width/2.+(world-centerX)*zoom);}
|
||||
private int screenZ(double world) {return (int)Math.round(mapCenterY()+(world-centerZ)*zoom);}
|
||||
private void renderGrid(GuiGraphicsExtractor g) {
|
||||
if(16*zoom<8)return;
|
||||
int minX=Math.floorDiv((int)Math.floor(centerX-width/2./zoom),16);
|
||||
int maxX=Math.floorDiv((int)Math.ceil(centerX+width/2./zoom),16)+1;
|
||||
int minZ=Math.floorDiv((int)Math.floor(centerZ-mapCenterY()/zoom),16);
|
||||
@@ -492,12 +506,12 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
for (int z=minZ;z<=maxZ;z++) {int sy=screenZ(z*16.);g.fill(0,sy,width,sy+1,0x407A91A0);}
|
||||
}
|
||||
private void renderFootprints(GuiGraphicsExtractor g) {
|
||||
if (footprintTexture==null) return;
|
||||
if (footprintTexture==null || detail>0) return;
|
||||
for (var key:footprintUpdates) paintFootprint(key);
|
||||
footprintUpdates.clear();
|
||||
if (footprintDirty) {footprintTexture.upload();footprintDirty=false;}
|
||||
if (footprints || ChunkPropertyClient.propertiesVisible()) g.blit(RenderPipelines.GUI_TEXTURED,FOOTPRINT_TEXTURE,screenX(originX),screenZ(originZ),0,0,
|
||||
(int)(SIZE*zoom),(int)(SIZE*zoom),SIZE,SIZE,SIZE,SIZE);
|
||||
(int)Math.round(SIZE*(1<<detail)*zoom),(int)Math.round(SIZE*(1<<detail)*zoom),SIZE,SIZE,SIZE,SIZE);
|
||||
}
|
||||
private void paintFootprint(fr.koka.sanctuary.atlas.AtlasStore.Key key) {
|
||||
int left=key.x()*16-originX,top=key.z()*16-originZ;
|
||||
@@ -512,7 +526,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
}
|
||||
}
|
||||
private void renderFootprintHint(GuiGraphicsExtractor g,int mouseX,int mouseY) {
|
||||
if (!footprints || !readable() || dragging || !inside(mouseX,mouseY) || overControl(mouseX,mouseY)) return;
|
||||
if (detail>0 || !footprints || !readable() || dragging || !inside(mouseX,mouseY) || overControl(mouseX,mouseY)) return;
|
||||
int wx=(int)Math.floor(centerX+(mouseX-width/2.)/zoom),wz=(int)Math.floor(centerZ+(mouseY-mapCenterY())/zoom);
|
||||
var key=new fr.koka.sanctuary.atlas.AtlasStore.Key(Math.floorDiv(wx,16),Math.floorDiv(wz,16));
|
||||
byte[] colors=surveyed.get(key);
|
||||
@@ -531,7 +545,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
}
|
||||
// Native client checks inspect the actual uploaded image's source pixels, including alpha.
|
||||
public int renderedPixelAt(int worldX,int worldZ) {
|
||||
int x=worldX-originX,z=worldZ-originZ;
|
||||
int x=Math.floorDiv(worldX-originX,1<<detail),z=Math.floorDiv(worldZ-originZ,1<<detail);
|
||||
return texture==null || x<0 || z<0 || x>=SIZE || z>=SIZE ? 0 : texture.getPixels().getPixel(x,z);
|
||||
}
|
||||
public int knownPixels() {return surveyed.values().stream().mapToInt(colors -> {int n=0;for (byte color:colors) if (color!=0) n++;return n;}).sum();}
|
||||
@@ -554,6 +568,7 @@ public final class SanctuaryAtlasScreen extends BlocodexFrame {
|
||||
public double centerX() {return centerX;}
|
||||
public double centerZ() {return centerZ;}
|
||||
public double zoom() {return zoom;}
|
||||
public int detail() {return detail;}
|
||||
public int altitude() {return altitude;}
|
||||
public java.util.List<fr.koka.sanctuary.atlas.AtlasMarker> markers() {return markers;}
|
||||
public String status() {return status;}
|
||||
|
||||
@@ -17,7 +17,36 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
}
|
||||
|
||||
public static void registerClient() {
|
||||
net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents.CLIENT_STARTED.register(client -> {
|
||||
try {
|
||||
DefaultServerList.install(client.gameDirectory.toPath());
|
||||
} catch (java.io.IOException | RuntimeException error) {
|
||||
org.slf4j.LoggerFactory.getLogger("Sanctuary").warn("Could not add the default server; existing favorites preserved", error);
|
||||
}
|
||||
});
|
||||
InsectRenderer.register();
|
||||
net.fabricmc.fabric.api.client.rendering.v1.ModelLayerRegistry.registerModelLayer(FlyingWhaleModel.LAYER_LOCATION, FlyingWhaleModel::createBodyLayer);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(fr.koka.sanctuary.whale.FlyingWhales.TYPE, FlyingWhaleRenderer::new);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
net.minecraft.world.entity.EntityTypes.ZOMBIE,InfectedRenderer::new);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
fr.koka.sanctuary.infected.Infected.TYPE, InfectedRenderer::new);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
fr.koka.sanctuary.infected.Infected.CHARGER,InfectedRenderer::new);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
fr.koka.sanctuary.infected.Infected.SPITTER,InfectedRenderer::new);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
fr.koka.sanctuary.infected.Infected.ACID_SPIT,ctx->new net.minecraft.client.renderer.entity.ThrownItemRenderer<>(ctx,.5F,true));
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
fr.koka.sanctuary.infected.Infected.ACID_ZONE,net.minecraft.client.renderer.entity.NoopRenderer::new);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
fr.koka.sanctuary.infected.Infected.BOOMER,InfectedRenderer::new);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
fr.koka.sanctuary.infected.Infected.SMOKER,InfectedRenderer::new);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
fr.koka.sanctuary.infected.Infected.SMOKER_LASSO,SmokerLassoRenderer::new);
|
||||
net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry.register(
|
||||
fr.koka.sanctuary.infected.Infected.HUNTER,InfectedRenderer::new);
|
||||
InsectSoundClient.register();
|
||||
WaystoneScreen.register();
|
||||
PropertyScreen.register();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.client.zipline.GrapplingLeashRenderUtil;
|
||||
import fr.koka.sanctuary.infected.SmokerLassoEntity;
|
||||
import net.minecraft.client.renderer.entity.*;
|
||||
import net.minecraft.client.renderer.entity.state.ThrownItemRenderState;
|
||||
|
||||
/** Reuses the zipline's taut, lit native leash segments for the physical lasso. */
|
||||
public final class SmokerLassoRenderer extends ThrownItemRenderer<SmokerLassoEntity> {
|
||||
public SmokerLassoRenderer(EntityRendererProvider.Context context) { super(context,.65F,false); }
|
||||
@Override public void extractRenderState(SmokerLassoEntity hook,ThrownItemRenderState state,float partial) {
|
||||
super.extractRenderState(hook,state,partial);var owner=hook.getOwner();
|
||||
var hold=owner instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity pet?owner.getPosition(partial).add(0,pet.visualHeight()*.85,0):owner==null?null:owner.getRopeHoldPosition(partial);
|
||||
state.leashStates=owner==null?null:GrapplingLeashRenderUtil.buildSegmentedLeash(hook.level(),hook.getPosition(partial),hook.getPosition(partial),hold);
|
||||
}
|
||||
@Override protected boolean affectedByCulling(SmokerLassoEntity hook) { return false; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import java.util.UUID;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** A shared wardrobe, selected once by the already persistent native/familiar identity. */
|
||||
public final class ZombieSkins {
|
||||
public static final int COUNT=9;
|
||||
public static int index(UUID identity) { return Math.floorMod(identity.hashCode(),COUNT); }
|
||||
public static Identifier texture(int index,boolean baby) {
|
||||
if(index<0||index>=COUNT)throw new IllegalArgumentException("Zombie skin index");
|
||||
return SanctuaryMod.id("textures/entity/zombies/zombie"+(index+1)+(baby?"_baby":"")+".png");
|
||||
}
|
||||
private ZombieSkins() { }
|
||||
}
|
||||
@@ -116,6 +116,10 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
mountedClimbing=false;
|
||||
var player=pilot();
|
||||
if(player==null){super.travel(input);return;}
|
||||
var cast=fr.koka.sanctuary.familiar.FamiliarBattle.state(player).cast;
|
||||
if(cast!=null&&cast.pet==this&&(fr.koka.sanctuary.familiar.FamiliarRush.applies(cast.technique)||fr.koka.sanctuary.familiar.FamiliarPounce.applies(cast.technique))){
|
||||
super.travel(Vec3.ZERO);return;
|
||||
}
|
||||
var keys=player.getLastClientInput();
|
||||
setYRot(player.getYRot());setXRot(player.getXRot()*.5F);yBodyRot=getYRot();yHeadRot=getYRot();
|
||||
double forward=(keys.forward()?1:0)-(keys.backward()?1:0),side=(keys.left()?1:0)-(keys.right()?1:0);
|
||||
@@ -150,7 +154,9 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
double z=mountRoom(player,player.position().add(x,y,delta.z))?delta.z:0;
|
||||
delta=new Vec3(x,y,z);setDeltaMovement(delta);
|
||||
}
|
||||
var beforeCenter=getBoundingBox().getCenter();var beforePosition=position();
|
||||
super.move(type,delta);
|
||||
if(type==MoverType.SELF&&!level().isClientSide())fr.koka.sanctuary.familiar.FamiliarPounce.moved(this,beforeCenter,beforePosition,delta);
|
||||
}
|
||||
@Override protected void registerGoals() {
|
||||
goalSelector.addGoal(-3,new Goal(){
|
||||
@@ -209,7 +215,11 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
fr.koka.sanctuary.familiar.FamiliarTechniques.advance(this);return;
|
||||
}
|
||||
}
|
||||
if(CarryService.carried(this)||MobHeadActions.propelling(this)){getNavigation().stop();return;}
|
||||
if(CarryService.carried(this)||MobHeadActions.propelling(this)){
|
||||
var cast=fr.koka.sanctuary.familiar.FamiliarBattle.state(player).cast;
|
||||
if(cast!=null&&fr.koka.sanctuary.familiar.FamiliarPounce.applies(cast.technique)){fr.koka.sanctuary.familiar.FamiliarPounce.cancel(cast);fr.koka.sanctuary.familiar.FamiliarBattle.state(player).cast=null;}
|
||||
getNavigation().stop();return;
|
||||
}
|
||||
boolean obstructed=!level.noBlockCollision(this,getBoundingBox().deflate(0.02));
|
||||
if(distanceToSqr(player)>(fr.koka.sanctuary.familiar.FamiliarBattle.fighting(player)?24*24:144) || getY()<level.getMinY() || obstructed) {
|
||||
if(tickCount%10==0)teleportNearOwner();
|
||||
|
||||
@@ -14,7 +14,7 @@ public final class HeadEquipment {
|
||||
public static ItemStack get(LivingEntity e){return e instanceof Player p?AccessoryAccess.of(p).getItem(AccessoryContainer.HEAD):e instanceof Mob m?mob(m).stack:ItemStack.EMPTY;}
|
||||
public static void set(LivingEntity e,ItemStack stack){
|
||||
if(e instanceof Player p)AccessoryAccess.of(p).setItem(AccessoryContainer.HEAD,stack);
|
||||
else if(e instanceof Mob m){HeadBlockService.release(m);mob(m).stack=stack;mob(m).saved=null;MobHeadService.track(m);MobHeadService.publish(m);}
|
||||
else if(e instanceof Mob m){HeadBlockService.release(m);mob(m).stack=stack;mob(m).saved=null;fr.koka.sanctuary.creeper.HappyCreepers.headChanged(m);MobHeadService.track(m);MobHeadService.publish(m);}
|
||||
}
|
||||
public static void replace(LivingEntity e,ItemStack expected,ItemStack replacement){
|
||||
if(e instanceof Player p)AccessoryAccess.of(p).replaceHeadState(expected,replacement);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package fr.koka.sanctuary.creeper;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.HeadCosmeticService;
|
||||
import fr.koka.sanctuary.cosmetics.HeadEquipment;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.monster.Creeper;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Item;
|
||||
|
||||
/** Happiness is derived from the existing authoritative head slot, never saved twice. */
|
||||
public final class HappyCreepers {
|
||||
public static final TagKey<Item> FLOWERS = TagKey.create(Registries.ITEM, Identifier.withDefaultNamespace("flowers"));
|
||||
public interface Access { void sanctuary$calm(); }
|
||||
private HappyCreepers() {}
|
||||
|
||||
public static boolean isFlower(ItemStack stack) { return stack.is(FLOWERS); }
|
||||
|
||||
public static boolean isHappy(Creeper creeper) {
|
||||
return isFlower(creeper.level().isClientSide()
|
||||
? HeadCosmeticService.appearance(creeper) : HeadEquipment.get(creeper));
|
||||
}
|
||||
|
||||
public static void headChanged(Mob mob) {
|
||||
if (mob instanceof Creeper creeper && isHappy(creeper)) ((Access) creeper).sanctuary$calm();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,11 @@ import static fr.koka.sanctuary.familiar.FamiliarProfile.Action;
|
||||
public final class FamiliarActions {
|
||||
private FamiliarActions(){}
|
||||
public static String validate(FamiliarTechniques.Cast c){
|
||||
if(FamiliarPounce.applies(c.technique)&&(!c.pet.onGround()||c.pet.isInWater()||c.pet.isVehicle()||c.pet.isPassenger()))return "blocked";
|
||||
if(c.technique.effect().equals("lasso")) {
|
||||
if(fr.koka.sanctuary.infected.SmokerLassoEntity.active(c.pet)!=null)return "preparing";
|
||||
if(c.pet.isPassenger()||c.pet.isVehicle()||c.target()!=null&&CompanionWorld.boss(c.target()))return "blocked";
|
||||
}
|
||||
var t=c.technique;var p=c.owner;var target=c.target();var pet=c.pet;
|
||||
if(!p.isAlive()||pet.isRemoved()||pet.level()!=p.level())return "unavailable";
|
||||
if(target!=null&&(target.distanceToSqr(pet)>Math.pow(t.range()+2,2)||!pet.hasLineOfSight(target)))return "blocked";
|
||||
@@ -46,6 +51,8 @@ public final class FamiliarActions {
|
||||
};
|
||||
}
|
||||
public static void execute(FamiliarTechniques.Cast c){
|
||||
if(c.technique.effect().equals("lasso")){fr.koka.sanctuary.infected.SmokerLassoEntity.launch(c.pet,c.target(),c);return;}
|
||||
if(c.technique.effect().equals("bile")){fr.koka.sanctuary.infected.BoomerBile.familiar(c);return;}
|
||||
var t=c.technique;var p=c.owner;var pet=c.pet;var target=c.target();
|
||||
switch(t.action()){
|
||||
case STRIKE,CHARGE,INTERCEPT->{
|
||||
|
||||
@@ -168,7 +168,7 @@ public final class FamiliarBattle {
|
||||
if(p.tickCount%20==0)send(p,"");
|
||||
}
|
||||
public static void release(ServerPlayer p){var w=world(p.level().getServer());var state=STATES.remove(p);if(w!=null&&state!=null&&state.individual!=null)w.active.remove(state.individual,p.getUUID());if(state!=null)state.cast=null;}
|
||||
public static void cancel(ServerPlayer p){var s=state(p);s.cast=null;s.guard=null;s.target=null;s.explicitTarget=false;s.threat=null;s.evadeUntil=0;s.unreachable=null;s.retryAfter=0;FamiliarTechniques.cancel(p);}
|
||||
public static void cancel(ServerPlayer p){var pet=FamiliarService.active(p);var rope=pet==null?null:fr.koka.sanctuary.infected.SmokerLassoEntity.active(pet);if(rope!=null)rope.discard();var s=state(p);FamiliarRush.cancel(s.cast);FamiliarPounce.cancel(s.cast);s.cast=null;s.guard=null;s.target=null;s.explicitTarget=false;s.threat=null;s.evadeUntil=0;s.unreachable=null;s.retryAfter=0;FamiliarTechniques.cancel(p);}
|
||||
public static void combat(ServerPlayer p){
|
||||
var until=now()+COMBAT_MILLIS;state(p).playerCombatUntil=until;p.setAttached(COMBAT_LOCK,until);var b=bond(p);if(b!=null)update(p,b.combat(until));
|
||||
}
|
||||
@@ -248,10 +248,11 @@ public final class FamiliarBattle {
|
||||
if(attacker instanceof FamiliarEntity familiar)attacker=familiar.owner();
|
||||
if(attacker instanceof LivingEntity living&&!hostile&&!enemy(p,living))return false;
|
||||
if(!FamiliarDuels.fighting(p)&&attacker instanceof LivingEntity living)automatic(p,living,FamiliarPersonality.Trigger.PET_HURT);
|
||||
state(p).cast=null;state(p).lastHurt=p.tickCount;combat(p);
|
||||
FamiliarPounce.cancel(state(p).cast);state(p).cast=null;state(p).lastHurt=p.tickCount;combat(p);
|
||||
float scale=FamiliarLevels.health(bond(p));
|
||||
float damage=FamiliarTechniques.incoming(pet,source,amount)/scale,health=Math.max(0,pet.getHealth()-damage);
|
||||
if(damage<=0)return false;
|
||||
var lasso=fr.koka.sanctuary.infected.SmokerLassoEntity.active(pet);if(lasso!=null)lasso.discard();
|
||||
if(FamiliarTechniques.current()!=null)FamiliarDuels.hit(FamiliarTechniques.current(),Math.min(pet.getHealth(),damage)*scale);
|
||||
update(p,bond(p).health(health,health<=0?now()+KO_MILLIS:0));
|
||||
pet.setHealth(health>0?health:.01F);pet.animateHurt(source.getSourcePosition()==null?0:p.getYRot());p.level().broadcastDamageEvent(pet,source);
|
||||
@@ -303,6 +304,7 @@ public final class FamiliarBattle {
|
||||
}
|
||||
case "signature","variant","work"->{
|
||||
if(fighting(p)){send(p,"in_combat");return;}
|
||||
if(action.equals("work")&&CompanionService.equipped(p)==null){send(p,"no_work");return;}
|
||||
if(action.equals("work")&&!CompanionService.record(p).unlocked()&&!ProgressionService.canOperate(p)){send(p,"work_locked");return;}
|
||||
if(action.equals("variant")&&(!b.learned()||!CompanionService.record(p).unlocked())){send(p,"variant_locked");return;}
|
||||
s.workMode=action.equals("work");update(p,b.variant(action.equals("variant")));
|
||||
|
||||
@@ -18,7 +18,7 @@ public final class FamiliarBrain {
|
||||
private FamiliarBrain(){}
|
||||
public static boolean controls(FamiliarEntity pet){
|
||||
var p=pet.owner();if(p==null||!FamiliarBattle.active(p)||CarryService.carried(pet))return false;
|
||||
var s=FamiliarBattle.state(p);return s.cast!=null||s.target!=null||!s.workMode&&guarding(p)||s.evadeUntil>p.tickCount||FamiliarFields.busy(p);
|
||||
var s=FamiliarBattle.state(p);return fr.koka.sanctuary.infected.SmokerLassoEntity.active(pet)!=null||s.cast!=null||s.target!=null||!s.workMode&&guarding(p)||s.evadeUntil>p.tickCount||FamiliarFields.busy(p);
|
||||
}
|
||||
private static final double HUNT_RADIUS=12, GUARD_RADIUS=6, GUARD_LEASH=10;
|
||||
public static boolean guarding(ServerPlayer p){
|
||||
@@ -77,7 +77,16 @@ public final class FamiliarBrain {
|
||||
if(s.order==FamiliarBattle.Order.ENGAGE)s.order=FamiliarBattle.Order.FOLLOW;
|
||||
}
|
||||
public static void tick(FamiliarEntity pet){
|
||||
var p=pet.owner();if(p==null)return;var s=FamiliarBattle.state(p);var profile=FamiliarBattle.profile(p);if(profile==null)return;
|
||||
var p=pet.owner();
|
||||
// A running goal can tick once after the equipped egg changes, before the
|
||||
// end-of-tick service retires its old visual and prepares the new bond.
|
||||
if(p==null||!FamiliarBattle.active(p)||!net.minecraft.world.item.ItemStack.isSameItemSameComponents(pet.egg(),FamiliarBattle.egg(p))){
|
||||
pet.getNavigation().stop();pet.setAggressive(false);return;
|
||||
}
|
||||
var s=FamiliarBattle.state(p);var profile=FamiliarBattle.profile(p);if(profile==null)return;
|
||||
if(fr.koka.sanctuary.infected.SmokerLassoEntity.active(pet)!=null) {
|
||||
pet.getNavigation().stop();pet.setSpeed(0);pet.setXxa(0);pet.setZza(0);pet.setDeltaMovement(0,pet.getDeltaMovement().y,0);return;
|
||||
}
|
||||
var personality=FamiliarPersonality.of(FamiliarBattle.bond(p));
|
||||
var initiative=personality.initiative(profile,s.explicitTarget||s.order==FamiliarBattle.Order.PROTECT);
|
||||
var target=FamiliarBattle.target(p);
|
||||
|
||||
@@ -56,9 +56,15 @@ public final class FamiliarCatalog {
|
||||
private static FamiliarCatalog bundled;
|
||||
public static FamiliarCatalog bundled(){
|
||||
if(bundled!=null)return bundled;
|
||||
try(var input=FamiliarCatalog.class.getResourceAsStream("/data/sanctuary/sanctuary_familiars/vanilla.json")){
|
||||
if(input==null)throw new IOException("Missing bundled familiar catalogue");
|
||||
bundled=new FamiliarCatalog(parse(new String(input.readNBytes(1048577),StandardCharsets.UTF_8)));return bundled;
|
||||
try{
|
||||
var profiles=new ArrayList<FamiliarProfile>();
|
||||
for(String file:List.of("vanilla.json","infected.json","charger.json","spitter.json","boomer.json","smoker.json","hunter.json")){
|
||||
try(var input=FamiliarCatalog.class.getResourceAsStream("/data/sanctuary/sanctuary_familiars/"+file)){
|
||||
if(input==null)throw new IOException("Missing bundled familiar catalogue: "+file);
|
||||
profiles.addAll(parse(new String(input.readNBytes(1048577),StandardCharsets.UTF_8)));
|
||||
}
|
||||
}
|
||||
bundled=new FamiliarCatalog(profiles);return bundled;
|
||||
}catch(IOException e){throw new IllegalStateException("Cannot read bundled familiar catalogue",e);}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public enum FamiliarPersonality {
|
||||
|
||||
public enum Trigger { NEARBY_HOSTILE, GUARD_INTRUDER, OWNER_ATTACK, OWNER_HURT, PET_HURT }
|
||||
private static final Set<String> FIERCE=Set.of("vindicator","pillager","evoker","ravager","piglin_brute",
|
||||
"zombie","husk","drowned","skeleton","stray","bogged","parched","wither_skeleton",
|
||||
"zombie","husk","drowned","charger","spitter","boomer","smoker","hunter","skeleton","stray","bogged","parched","wither_skeleton",
|
||||
"creeper","blaze","breeze","ghast","wither","ender_dragon","warden","zoglin");
|
||||
private static final Set<String> GENTLE=Set.of("villager","wandering_trader","allay","cow","mooshroom",
|
||||
"sheep","pig","chicken","rabbit","turtle","tadpole","horse","donkey","mule","happy_ghast");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user