Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309d0ba6f3 | ||
|
|
5dee98106e | ||
|
|
d6f3258442 | ||
|
|
1c8e6437d5 |
@@ -6,16 +6,29 @@ 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).
|
||||
|
||||
## Essai de carte derrière Pause — annulé
|
||||
## beta.238 — Forgerie
|
||||
|
||||
[L'essai plein écran](docs/pause-map-layer-beta236.md) est annulé à la demande
|
||||
du créateur. Pause retrouve la disposition beta.235, avec la carte au centre
|
||||
et une colonne de navigation à gauche lorsque l'Atlas est déverrouillé.
|
||||
Story et **M**, configurable dans les contrôles, sont conservés.
|
||||
Parcours client Vulkan FR/EN GUI 2/3/4 et `check build` de l'annulation réussis.
|
||||
L'ancien pack d'essai beta.236 est retiré des artefacts actifs ; aucune nouvelle
|
||||
livraison n'est créée par cette annulation. La future livraison commune avec
|
||||
les aptitudes et Forgerie beta.238 devra utiliser le prochain compteur global.
|
||||
[Aptitude Forgerie](docs/forgerie-beta238.md) dans Arts et métiers : quatre
|
||||
niveaux pour débloquer les enclumes et la table de forge, achat définitif.
|
||||
Les réparations, noms, améliorations en netherite et ornements conservent
|
||||
leurs règles natives. L'achat est contrôlé côté serveur, y compris pour les
|
||||
postes portés sur la tête. Icône fournie et libellés FR/EN intégrés.
|
||||
Build, 21 GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
|
||||
[MRpack local beta.238](build/Sanctuary-beta.238.mrpack) vérifié, sans
|
||||
publication ni mise à jour personnelle.
|
||||
|
||||
## beta.237 — Aptitudes acquises définitivement
|
||||
|
||||
[Aptitudes par thème et achats permanents](docs/aptitudes-beta237.md) : seuls
|
||||
World Map et Mob Names sont activables/désactivables. Les dix autres aptitudes
|
||||
restent utilisables après achat ; Enchantement et Alchimie coûtent chacun
|
||||
quatre niveaux et débloquent l'accès personnel aux postes natifs. Icônes
|
||||
fournies intégrées, libellés FR/EN. Les anciens choix de désactivation de ces
|
||||
achats sont effacés sans toucher aux achats ni à l'XP. Disposition Pause
|
||||
beta.235 intégrée, adaptée au choix de masquer la carte. `check build assemblePack`,
|
||||
37 GameTests requis et parcours Vulkan FR/EN GUI 2/3/4 réussis.
|
||||
[MRpack local beta.237](build/Sanctuary-beta.237.mrpack) vérifié ; aucune
|
||||
publication ni mise à jour personnelle.
|
||||
|
||||
## beta.235 — Essai réversible du menu Pause
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# APT-236 — aptitudes par thème et activation personnelle
|
||||
|
||||
La règle de désactivation décrite ici correspond à la livraison beta.236.
|
||||
[Beta.237](aptitudes-beta237.md) la remplace : seuls World Map et Mob Names
|
||||
restent désactivables, les autres aptitudes sont acquises définitivement.
|
||||
Le chantier Pause beta.235 est intégré dans cette nouvelle livraison.
|
||||
|
||||
Branche `codex/progression-aptitudes`, base beta.234 (`f9c2ce8`). Le travail
|
||||
parallèle du menu Pause occupe beta.235 ; ce lot réserve beta.236.
|
||||
|
||||
## Contrat avant implémentation
|
||||
|
||||
Progression regroupe les aptitudes en cinq thèmes : Survie (Swimming, Rest,
|
||||
Food Knowledge), Inventaire et fabrication (Inventory Sorting, Catalogue),
|
||||
Familiers (Active Bond, Familiar Study), Exploration (World Map, Mob Names,
|
||||
Zoom), Arts et métiers (Enchantement, Alchimie). Le minage et la forgerie
|
||||
restent hors de ce ticket.
|
||||
|
||||
Enchantement et Alchimie coûtent chacun 4 niveaux, comme les aptitudes actuelles.
|
||||
L'usage personnel de la table d'enchantement et de l'alambic est verrouillé au
|
||||
départ dans les mondes avec progression Sanctuary. Les menus natifs sont
|
||||
refusés côté serveur tant que l'aptitude correspondante n'est pas acquise et
|
||||
active, y compris via les blocs fonctionnels portés sur la tête. Les enclumes,
|
||||
la table de forge, les équipements enchantés et les potions déjà obtenues
|
||||
restent inchangés. Les alambics partagés et leur automatisation continuent
|
||||
leur fonctionnement autonome : ce lot contrôle l'accès personnel au poste,
|
||||
sans attribuer de propriétaire à un bloc ni arrêter une préparation lancée.
|
||||
Les personnages existants avec progression Sanctuary doivent aussi acquérir
|
||||
ces deux nouvelles aptitudes ; aucune acquisition gratuite n'est ajoutée.
|
||||
|
||||
Une aptitude achetée peut être activée ou désactivée sans coût et sans perdre
|
||||
son achat. Food Knowledge reste permanent après apprentissage. Les compétences,
|
||||
les prix et les découvertes ne changent pas. Les effets de gameplay sont
|
||||
contrôlés côté serveur ; les aides d'interface utilisent le choix serveur.
|
||||
Active Bond contrôle l'usage des pouvoirs de travail, pas les techniques de
|
||||
combat, les pouvoirs passifs ou l'équipement du familier. Une action déjà
|
||||
lancée se termine normalement ; les nouvelles activations sont refusées.
|
||||
|
||||
## Persistance et compatibilité
|
||||
|
||||
L'attachement additif `sanctuary:aptitude_preferences` conserve un entier de
|
||||
0 à 2047 : un bit par aptitude désactivée, dans l'ordre immuable names, atlas,
|
||||
sorting, swimming, resting, active_bond, familiar_study, catalogue, zoom,
|
||||
enchanting, alchemy.
|
||||
L'absence de cet attachement équivaut à zéro : toutes les aptitudes acquises
|
||||
gardent leur comportement actuel. Il est copié à la mort et conservé au New
|
||||
Game+. Deux attachements booléens additifs `sanctuary:enchanting_learned` et
|
||||
`sanctuary:alchemy_learned` conservent leurs nouveaux achats ; leur absence
|
||||
équivaut à false et ils sont copiés à la mort et conservés au New Game+.
|
||||
Aucun schéma ou contenu des achats existants n'est réécrit. Ce contrat
|
||||
est limité à cet attachement ; aucun monde personnel n'est ouvert ou modifié.
|
||||
|
||||
Une requête transmet la cible, le choix et le masque attendu ; le serveur
|
||||
vérifie l'achat, la session, l'état du joueur et le masque courant. Un choix
|
||||
inconnu, Food Knowledge, une aptitude non acquise ou une requête périmée est
|
||||
refusé sans XP dépensée. Un état distinct est renvoyé avant le snapshot de
|
||||
progression. La capacité réseau `aptitude_preferences_v1` exige les versions
|
||||
compatibles sur client et serveur sans changer les paquets existants.
|
||||
|
||||
Désactiver Swimming interrompt le sprint aquatique ; désactiver Rest arrête
|
||||
la récupération. World Map masque son entrée et refuse les nouvelles lectures
|
||||
personnelles ; les relevés continuent d'être conservés et l'outil opérateur
|
||||
reste disponible. Catalogue masque l'aide personnelle JEI et la consultation
|
||||
complète de Discovery, sans toucher aux recettes ni aux découvertes.
|
||||
|
||||
Un ancien mod ignore ces trois nouveaux attachements et retrouve son ancien
|
||||
accès aux postes. Il peut perdre les attachements inconnus en sauvegardant :
|
||||
conserver une sauvegarde avant un retour arrière. Le schéma 9 de progression
|
||||
et les preuves des aptitudes antérieures restent identiques.
|
||||
|
||||
## Vérifications
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=aptitudes236,progression,movement,sorting,recipes,foodknowledge \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
|
||||
```
|
||||
|
||||
Première passe serveur : `check build assemblePack` réussi en 3 min 13 s,
|
||||
**33 GameTests requis réussis**, dont cinq scénarios APT-236. Contrôles purs
|
||||
du dépôt réussis. Le banc Atlas existant évite le conflit de génération du
|
||||
chemin par défaut déjà documenté en beta.232. Monde de développement neuf.
|
||||
Log : `build/aptitudes236-check-build-pack.log`.
|
||||
|
||||
- Onze choix indépendants, refus des requêtes inconnues, non acquises,
|
||||
périmées et de Food Knowledge ; conservation exacte de l'XP et des preuves
|
||||
d'achat. Achat à 3 niveaux refusé, à 4 accepté une seule fois.
|
||||
- Aller-retour des nouveaux attachements dans le NBT joueur natif, copie à
|
||||
la mort, conservation lors de la remise à zéro des compétences du cycle.
|
||||
- Swimming arrête le sprint aquatique et interdit les flags de mouvement
|
||||
directs ; Rest arrête la récupération ; tri falsifié et lectures Atlas
|
||||
personnelles refusés. Réactivation du tri contrôlée avec la même requête.
|
||||
Active Bond refuse une nouvelle activation avant toute planification.
|
||||
- Table d'enchantement native refusée avant acquisition, ouverte ensuite ;
|
||||
**une épée diamant est réellement enchantée**. Désactiver ferme le menu,
|
||||
refuse sa réutilisation et son bouton d'enchantement sans dépense d'XP.
|
||||
Alambic natif refusé puis ouvert, fermeture et invalidation à la désactivation,
|
||||
ingrédients partagés conservés et réouverture après réactivation.
|
||||
|
||||
Parcours client final réussi en **52 s**, Minecraft 26.3, Vulkan confirmé par
|
||||
MoltenVK 1.4.2 / Apple M1. Log : `build/aptitudes236-client-vulkan.log`, marqueur
|
||||
`APTITUDES236_PASS` ; douze captures dans `build/aptitudes236-screenshots/`.
|
||||
|
||||
- FR/EN, GUI 2/3/4, fenêtre 1280 × 960 : cinq thèmes dans l'ordre, les douze
|
||||
aptitudes présentes, Food Knowledge permanent et sans interrupteur.
|
||||
- Achat des deux arts depuis les véritables boutons de Progression : huit
|
||||
niveaux au total. **66 paires désactivation/réactivation** via les widgets
|
||||
et le réseau, état serveur contrôlé, XP et progression finale identiques.
|
||||
- Libellés d'état entièrement contenus dans les boutons ; défilement et
|
||||
position conservée au redimensionnement 1344 × 1008 ; Terminé revient à
|
||||
Pause. Une première passe a trouvé le libellé permanent trop large en GUI 4 :
|
||||
retour à la ligne ajouté. Les interrupteurs affichent un état court et
|
||||
expliquent leur action gratuite dans l'infobulle.
|
||||
|
||||
Les essais concernent les sauvegardes de développement. Pas de validation
|
||||
Windows ou multijoueur distant. L'automatisation des alambics et les pouvoirs
|
||||
déjà lancés restent les limites explicites du contrat ; la forgerie attend
|
||||
son prochain ticket.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
Build final `check build assemblePack` réussi en **2 min 24 s**, 136 tâches,
|
||||
33/33 GameTests requis et contrôles purs réussis après les derniers réglages
|
||||
d’interface. Log : `build/aptitudes236-check-build-pack-final.log`.
|
||||
|
||||
Versions du mod, du pack et du manifeste alignées sur **beta.236**.
|
||||
[MRpack local](../build/Sanctuary-beta.236.mrpack) vérifié : 12797046 octets,
|
||||
intégrité ZIP, Minecraft 26.3 / Fabric Loader 0.19.5, un seul JAR Sanctuary
|
||||
identique au build final, sans classe de test ni monde ni module de laboratoire.
|
||||
Les deux PNG fournis sont identiques octet pour octet aux originaux dans le JAR.
|
||||
Reçu : `build/aptitudes236-artifact.json`.
|
||||
|
||||
- SHA-256 JAR : `21a4411ebed223a1d9f9d204dc9d7300af0d5b946416e39bb138f5c575bf5847`.
|
||||
- SHA-256 MRpack : `9d3eca559805d451e8c653cca1d59186ae46e3c6964147b9fc072b9da832f921`.
|
||||
|
||||
La branche reste indépendante du chantier Pause beta.235 ; leur intégration
|
||||
reste à effectuer. Aucune publication distante, mise à jour du canal packwiz
|
||||
ou synchronisation d’instance personnelle.
|
||||
@@ -0,0 +1,99 @@
|
||||
# APT-237 — achats permanents et intégration du menu Pause
|
||||
|
||||
Branche `codex/progression-aptitudes`. Intégration des commits beta.235
|
||||
`a7bdfe6` et `00d175c`, avec les aptitudes beta.236 `1c8e643`.
|
||||
|
||||
## Contrat avant implémentation
|
||||
|
||||
Seuls World Map et Mob Names disposent d'un interrupteur gratuit après achat.
|
||||
Swimming, Rest, Inventory Sorting, Catalogue, Active Bond, Familiar Study,
|
||||
Zoom, Enchantement, Alchimie et Food Knowledge restent acquis et utilisables
|
||||
sans désactivation. Les prix et les conditions d'achat restent identiques.
|
||||
Enchantement et Alchimie exigent toujours leur achat avant l'accès personnel
|
||||
au poste natif. Les achats, l'XP et les preuves existantes sont conservés.
|
||||
|
||||
Le menu Pause reprend la disposition beta.235 : deux colonnes Aventure et
|
||||
Communauté avant World Map, une colonne à gauche avec la carte. Le choix
|
||||
personnel de masquer World Map suit aussi cette disposition.
|
||||
|
||||
## Contrat de migration des préférences beta.236
|
||||
|
||||
L'identifiant `sanctuary:aptitude_preferences`, son codec entier 0–2047 et
|
||||
l'ordre historique des onze bits restent stables. Les bits 0 (Mob Names) et
|
||||
1 (World Map) restent effectifs. Les bits 2 à 10 deviennent inertes et sont
|
||||
retirés de cet attachement lors de sa prochaine synchronisation serveur.
|
||||
Ainsi un ancien masque 2047 devient 3 : les deux aides restent masquées et
|
||||
les neuf autres aptitudes achetées retrouvent automatiquement leur usage.
|
||||
Le chargement accepte encore les anciennes valeurs ; les lectures et effets
|
||||
ignorent immédiatement leurs bits désormais inertes. Aucune preuve d'achat,
|
||||
aucun format de progression, aucun monde ou terrain n'est réécrit.
|
||||
|
||||
Les requêtes de désactivation des aptitudes permanentes sont refusées même
|
||||
si elles proviennent d'un ancien client. Les paquets v1 restent inchangés ;
|
||||
une capacité additive `sanctuary:aptitude_permanent_v1` impose un client avec
|
||||
cette règle côté serveur. Les nouvelles interfaces sont traduites FR/EN.
|
||||
Le retour à une ancienne version nécessite comme auparavant une sauvegarde,
|
||||
car cette version ne peut restaurer les anciennes désactivations effacées.
|
||||
|
||||
## Vérification et livraison
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=aptitudes236,menus,progression,movement,sorting,recipes,foodknowledge \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
|
||||
```
|
||||
|
||||
Les suites APT-236 sont actualisées pour vérifier la nouvelle règle et la
|
||||
migration ; leurs noms Gradle restent stables. `check build assemblePack`
|
||||
réussi en **2 min 34 s**, **37 GameTests requis réussis**, dont six scénarios
|
||||
aptitudes. Contrôles purs du dépôt réussis. Le banc Atlas évite le chemin de
|
||||
génération par défaut déjà documenté en beta.232. Log :
|
||||
`build/aptitudes237-check-build-pack.log`.
|
||||
|
||||
- Les deux choix personnels sont gratuits ; refus des requêtes périmées,
|
||||
inconnues, non acquises et des désactivations d'aptitudes permanentes.
|
||||
Prix des nouveaux achats vérifié à trois/quatre niveaux, achat unique.
|
||||
- Ancien masque 2047 rechargé dans le NBT natif, interprété en 3 puis
|
||||
normalisé sans changement d'XP ni de preuve d'achat. Conservation des
|
||||
choix et des achats à la mort et au New Game+.
|
||||
- Avec d'anciens bits désactivés : sprint aquatique, Rest et tri restent
|
||||
utilisables ; Active Bond suit son contrôle normal de familier équipé.
|
||||
World Map conserve son contrôle d'accès et sa réactivation gratuite.
|
||||
- Enchantement et Alchimie refusés avant achat, puis disponibles lors des
|
||||
visites suivantes. Une épée diamant est réellement enchantée. Une requête
|
||||
de désactivation refusée ne ferme ni n'invalide les menus acquis ; ingrédients
|
||||
partagés de l'alambic conservés. Accès vanilla hors progression conservé.
|
||||
|
||||
Parcours client réussi en **32 s**, Minecraft 26.3, Vulkan / MoltenVK 1.4.2
|
||||
sur Apple M1. FR/EN, GUI 2/3/4, fenêtre 1280 × 960 : cinq thèmes, douze
|
||||
aptitudes, dix achats permanents, **12 paires désactivation/réactivation**
|
||||
réelles des deux aides. Deux achats des arts via les boutons natifs de
|
||||
Progression, huit niveaux au total. Libellés contenus dans leurs boutons,
|
||||
XP et preuves conservées, défilement conservé au redimensionnement et retour
|
||||
Terminé vers Pause.
|
||||
|
||||
La même suite réutilise les assertions Pause du collègue : disposition,
|
||||
ordre clavier, marges, titres et libellés de navigation vérifiés après achat
|
||||
de World Map, avec son choix personnel désactivé puis activé. Les deux
|
||||
colonnes et la colonne près de la carte sont conservées. Les captures de
|
||||
Pause portent sur la disposition ; les relevés et fils commencent à charger.
|
||||
Log `build/aptitudes237-client-vulkan.log`, marqueur `APTITUDES237_PASS` ;
|
||||
**24 captures** dans `build/aptitudes237-screenshots/`. Inspection des captures
|
||||
Arts FR GUI 3, Pause sans carte FR GUI 4 et Pause avec carte EN GUI 3.
|
||||
|
||||
Versions alignées sur **beta.237**. [MRpack local](../build/Sanctuary-beta.237.mrpack)
|
||||
vérifié : 12797901 octets, intégrité ZIP, Minecraft 26.3 / Fabric Loader 0.19.5,
|
||||
un seul JAR Sanctuary identique au build final, sans tests ni monde ni module
|
||||
de laboratoire. Les deux icônes fournies restent identiques octet pour octet
|
||||
aux originaux. Reçu `build/aptitudes237-artifact.json`.
|
||||
|
||||
- SHA-256 JAR : `7ac741c6a15b8ab05a909cf0132c9d605b863723c0742a92d8b60c741fd760a6`.
|
||||
- SHA-256 MRpack : `7635e9e7a425cbd7a7f6d020f9bb2bcced8cc1287967093346295e4b4b2250e9`.
|
||||
|
||||
Les anciens artefacts beta.236 sont conservés. Tests dans des mondes de
|
||||
développement neufs ; Windows et multijoueur distant non testés. Les
|
||||
alambics autonomes et pouvoirs déjà lancés gardent le contrat beta.236.
|
||||
La forgerie, les enclumes et la table de forge attendent leur prochain ticket.
|
||||
Aucune publication, avancée du canal ni synchronisation d'installation personnelle.
|
||||
+15
-6
@@ -1,12 +1,21 @@
|
||||
# Backlog Sanctuary
|
||||
|
||||
## PAUSE-236 — Atlas plein écran derrière Pause — essai annulé
|
||||
## APT-238 — Forgerie — livré localement
|
||||
|
||||
Le créateur annule uniquement la carte derrière le menu. Le commit de
|
||||
disposition `5ea7950` est inversé ; affichage beta.235 restauré, Story et
|
||||
raccourci M (`94fefc0`) conservés. [Historique et annulation](pause-map-layer-beta236.md).
|
||||
L'ancien pack local est retiré des artefacts actifs. Cette annulation ne publie
|
||||
aucune nouvelle version et ne modifie pas la branche des aptitudes/Forgerie.
|
||||
Branche `codex/forgerie-beta238`, base beta.237. Aptitude permanente des Arts
|
||||
et métiers ; quatre niveaux pour les trois variantes d'enclume et la table
|
||||
de forge, y compris les postes portés. [Contrat et persistance](forgerie-beta238.md).
|
||||
Réparations, noms, améliorations et ornements gardent leurs règles natives.
|
||||
Build, 21 GameTests requis, Vulkan FR/EN GUI 2/3/4 et MRpack vérifiés.
|
||||
|
||||
## APT-237 — achats permanents et intégration Pause — livré localement
|
||||
|
||||
Branche `codex/progression-aptitudes`, travaux beta.235 et beta.236 réunis.
|
||||
Cinq thèmes, douze aptitudes ; seuls World Map et Mob Names ont un interrupteur.
|
||||
Enchantement et Alchimie restent à acheter avant l'accès personnel au poste.
|
||||
[Contrat et migration](aptitudes-beta237.md) des préférences antérieures,
|
||||
avec conservation des achats et de l'XP. Build, 37 GameTests requis,
|
||||
Vulkan FR/EN GUI 2/3/4 et MRpack local vérifiés. La forgerie attend ses précisions.
|
||||
|
||||
## PAUSE-235 — groupes côte à côte avant Atlas — essai livré localement
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
# APT-238 — Forgerie
|
||||
|
||||
Branche `codex/forgerie-beta238`, base beta.237 (`5dee981`).
|
||||
|
||||
## Contrat avant implémentation
|
||||
|
||||
Une aptitude Forgerie est ajoutée dans Arts et métiers de Progression, avec
|
||||
l'icône `aptitudes_forgerie.png` fournie. Comme Enchantement et Alchimie,
|
||||
elle coûte quatre niveaux, est achetée une seule fois et reste acquise
|
||||
sans interrupteur. Elle débloque l'usage personnel de toutes les variantes
|
||||
d'enclume et de la table de forge. Avant l'achat, les menus natifs de ces
|
||||
postes sont refusés côté serveur, y compris les postes fonctionnels portés
|
||||
sur la tête. Les réparations, noms, améliorations et ornements natifs restent
|
||||
fonctionnels après achat. Équipements déjà obtenus et recettes conservés.
|
||||
|
||||
La meule, les fours et les autres postes restent sous leurs règles actuelles.
|
||||
Les deux aides World Map / Mob Names restent les seuls choix désactivables.
|
||||
Les interfaces ajoutées sont traduites en français et en anglais.
|
||||
|
||||
## Persistance et compatibilité
|
||||
|
||||
L'attachement booléen additif `sanctuary:smithing_learned` conserve l'achat.
|
||||
Absent : false. Copie à la mort et conservation au New Game+. Aucun schéma,
|
||||
aucune preuve ni aucun attachement existant n'est réécrit. Les personnages
|
||||
existants avec progression doivent aussi acquérir Forgerie. Les joueurs
|
||||
hors progression Sanctuary gardent l'accès vanilla. Aucun monde personnel,
|
||||
aucune génération ni aucun terrain n'est modifié.
|
||||
|
||||
La capacité `sanctuary:smithing_v1` et l'état `sanctuary:smithing_state_v1`
|
||||
transmettent l'achat sans modifier
|
||||
les paquets v1 existants. Le serveur exige un client compatible et contrôle
|
||||
la session, le joueur, la révision et les niveaux avant l'achat. Un retour
|
||||
à une version ancienne peut perdre cet attachement inconnu à la sauvegarde ;
|
||||
conserver une sauvegarde avant ce retour.
|
||||
|
||||
## Vérifications et livraison
|
||||
|
||||
```sh
|
||||
./gradlew check build assemblePack \
|
||||
-PsanctuaryFocusedTests=forgerie238,aptitudes236,headblocks \
|
||||
-PsanctuaryAtlasOnly=true
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true \
|
||||
-PsanctuaryAptitudes236ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
|
||||
```
|
||||
|
||||
`check build assemblePack` réussi en **2 min 44 s**, **21 GameTests requis**
|
||||
réussis : cinq nouveaux scénarios Forgerie, six scénarios d'aptitudes et dix
|
||||
scénarios de postes portés. Contrôles purs du dépôt réussis. Le banc Atlas
|
||||
évite le chemin de génération par défaut déjà documenté en beta.232.
|
||||
Log `build/forgerie238-check-build-pack.log`.
|
||||
|
||||
- Achat refusé avec trois niveaux ou une révision périmée ; quatre niveaux
|
||||
dépensés une seule fois. Autres achats et preuves inchangés. Requête de
|
||||
désactivation refusée ; conservation native NBT, à la mort et au New Game+.
|
||||
- Trois variantes d'enclume refusées avant achat puis ouvertes après achat.
|
||||
**Réparation et renommage réels d'une épée diamant** sur chacune ; XP et
|
||||
diamant consommés selon le coût natif du menu.
|
||||
- Table de forge refusée avant achat puis ouverte : **épée netherite et
|
||||
plastron diamant avec ornement Sentry réellement produits**, entrées
|
||||
consommées une fois, coût d'XP natif conservé et accès aux visites suivantes.
|
||||
- Menus construits en dehors de l'ouverture normale : validité et prise de
|
||||
résultat refusées avant acquisition, sans dépense ni perte d'entrées.
|
||||
Joueurs sans progression Sanctuary autorisés comme en vanilla.
|
||||
- Enclumes et table de forge sur la tête : contrôle de l'achat du visiteur,
|
||||
indépendamment du porteur. La fixture générale des postes natifs possède
|
||||
explicitement les trois aptitudes des Arts avant de tester leurs menus.
|
||||
|
||||
Sources et bytecode du **Minecraft 26.3 local** vérifiés pour les méthodes
|
||||
héritées de `ItemCombinerMenu`, l'override de prise d'`AnvilMenu` et les recettes
|
||||
natives de forge. Aucun changement de recette ni de dépendance externe.
|
||||
|
||||
Parcours graphique en **34 s**, Vulkan / MoltenVK 1.4.2 sur Apple M1,
|
||||
FR/EN GUI 2/3/4. La suite d'aptitudes existante contrôle désormais treize
|
||||
aptitudes, dont onze achats permanents. Trois Arts achetés depuis les boutons
|
||||
natifs : douze niveaux au total, Forgerie synchronisée depuis le serveur et
|
||||
présentée comme acquise sans interrupteur. Deux seules aides désactivables,
|
||||
XP et progression conservées lors de douze paires de changement, disposition
|
||||
Pause intégrée, défilement et retour Terminé conservés. Log
|
||||
`build/forgerie238-client-vulkan.log`, marqueur `FORGERIE238_PASS`.
|
||||
Vingt-quatre captures dans `build/forgerie238-screenshots/` ; Arts FR GUI 3
|
||||
et EN GUI 4 inspectés, texte et icône contenus dans le cadre.
|
||||
|
||||
Versions du mod, du pack et du manifeste alignées sur **beta.238**.
|
||||
[MRpack local](../build/Sanctuary-beta.238.mrpack) vérifié : 12802487 octets,
|
||||
intégrité ZIP, Minecraft 26.3 / Fabric Loader 0.19.5, un seul JAR Sanctuary
|
||||
identique au build final, sans classe de test, monde ou module de laboratoire.
|
||||
Les trois icônes fournies des Arts sont identiques octet pour octet aux
|
||||
originaux dans le JAR. Reçu `build/forgerie238-artifact.json`.
|
||||
|
||||
- SHA-256 JAR : `e29ea6f7f56c57290d0307076f641f2e714ec72a80be858a5fd9c5f08e27eb8e`.
|
||||
- SHA-256 MRpack : `2fbd605acf555dd8b192c0c0ad7171414a13f5820e5d440ba54debcd4cb897a7`.
|
||||
|
||||
Tests dans des mondes de développement neufs. Pas de validation Windows ni
|
||||
multijoueur distant. Les sauvegardes et installations personnelles restent
|
||||
intactes ; aucune publication ni avancée du canal packwiz.
|
||||
@@ -1,157 +0,0 @@
|
||||
# PAUSE-236 — Atlas plein écran derrière le menu
|
||||
|
||||
## Statut : essai annulé le 6 octobre 2026
|
||||
|
||||
Le créateur demande d'annuler **uniquement la carte derrière Pause**.
|
||||
Le patch de disposition `5ea7950` est inversé, sans réécrire l'historique :
|
||||
carte centrale et colonne Communauté → Aventure → Game à gauche lorsque
|
||||
l'Atlas est déverrouillé, comme en beta.235. Story, la disposition sans Atlas
|
||||
et le raccourci M (`94fefc0`) restent présents. Le Nord retrouve sa position
|
||||
dans le panneau de carte. Aucun changement sur les autres branches.
|
||||
|
||||
Les vérifications et la livraison ci-dessous décrivent l'essai historique,
|
||||
avant annulation. Son pack est déplacé dans
|
||||
`build/withdrawn/Sanctuary-beta.236.mrpack` pour ne pas le proposer comme résultat
|
||||
actuel ; le staging packwiz de cet essai et son JAR d'origine sont également
|
||||
archivés dans `build/withdrawn/`. Le numéro beta.236 était également utilisé par la branche des aptitudes,
|
||||
qui a depuis atteint beta.238 avec Forgerie : cette branche d'essai ne constitue
|
||||
pas la livraison commune. Aucun nouvel artefact de distribution n'est livré
|
||||
par l'annulation ; la consolidation et son prochain compteur global restent
|
||||
un travail distinct.
|
||||
|
||||
Le parcours client Vulkan du retour réussit en 46 s : FR/EN GUI 2/3/4, géométrie
|
||||
beta.235 avec/sans Atlas, Story et ses 100 entrées, recherche, filtres, retours,
|
||||
focus et redimensionnement. M depuis le jeu et Pause, verrou de progression,
|
||||
réaffectation et désactivation passent également. Log :
|
||||
`build/pause-map-layer-cancel-client-vulkan.log`.
|
||||
Comparaison Git : les deux fichiers de disposition/carte et leurs assertions
|
||||
Pause/Solo sont identiques à beta.235 ; implémentation du raccourci et libellés
|
||||
FR/EN identiques au commit `94fefc0`.
|
||||
|
||||
`./gradlew check build -PsanctuaryFocusedTests=menus -PsanctuaryAtlasOnly=true`
|
||||
réussit en 2 min 34 s : quatre GameTests requis et contrôles purs du dépôt.
|
||||
Log : `build/pause-map-layer-cancel-check-build.log`. Aucun nouvel assemblage
|
||||
de distribution : il s'agit de l'annulation ciblée des sources de l'essai.
|
||||
|
||||
## Historique de l'essai
|
||||
|
||||
Branche d'essai `codex/pause-map-layer-beta236`, base beta.235, commit `00d175c`,
|
||||
Minecraft 26.3. Le rendu reste à apprécier par le créateur.
|
||||
|
||||
## Résultat
|
||||
|
||||
Gazette reste à gauche et Notice Board à droite, sur toute la hauteur du corps
|
||||
de Pause, avec ou sans Atlas. Au centre, Aventure (Discovery, Progression,
|
||||
Story) et Communauté (Habitant, Faction, Battle) sont côte à côte ; Partie /
|
||||
Game suit juste dessous. Reprendre la partie garde le pied de page natif.
|
||||
|
||||
Avec Atlas, la carte occupe littéralement tout l'écran sur le calque derrière
|
||||
les contrôles. Le terrain est légèrement assombri pour lire les libellés. Le
|
||||
Nord `N ↑` se trouve au centre horizontal, juste sous le bandeau du message
|
||||
serveur ; un espace réservé le sépare du menu. Les boutons et espacements
|
||||
s'adaptent aux petites hauteurs.
|
||||
|
||||
La carte est rendue avant les widgets de premier plan, puis reçoit les entrées
|
||||
après eux. Un clic dans une zone libre ouvre l'Atlas complet ; les boutons,
|
||||
les fils, l'en-tête et le pied de page gardent leur priorité. Les infobulles
|
||||
et les marqueurs de la carte ne capturent pas le survol derrière un contrôle.
|
||||
Les marqueurs suivis restent accessibles lorsqu'ils sont exposés. Tab après
|
||||
Quit atteint la carte ; Entrée ouvre l'Atlas, Échap ou Terminé revient à la
|
||||
même Pause. Redimensionner reconstruit le calque aux dimensions de l'écran.
|
||||
|
||||
Seul le terrain personnel déjà cartographié est affiché. Ce changement ne
|
||||
révèle ni ne génère de nouveaux chunks. Les contrats serveur, sauvegardes,
|
||||
progression, données communautaires et contenus Story restent identiques.
|
||||
Les libellés FR/EN existants sont réutilisés pour la disposition.
|
||||
|
||||
## Raccourci carte
|
||||
|
||||
La lettre **M** ouvre l'Atlas complet depuis le jeu et depuis Pause, une fois
|
||||
la carte déverrouillée dans Progression. Avant ce déblocage, elle affiche le
|
||||
message existant rappelant cette condition. Depuis le jeu, Échap et Terminé
|
||||
reviennent au jeu ; depuis Pause, ils reviennent à la même Pause.
|
||||
|
||||
Le raccourci figure dans les contrôles natifs, catégorie Sanctuary, sous
|
||||
« Ouvrir la carte » / « Open world map ». Il suit la lettre M de la disposition
|
||||
du clavier, y compris AZERTY, et préserve une affectation sauvegardée ou
|
||||
désactivée. Les écrans de saisie conservent leurs touches. Aucun nouveau
|
||||
protocole, fichier persistant ou déblocage serveur n'est ajouté.
|
||||
|
||||
## Réversibilité
|
||||
|
||||
Le code de l'essai et ses assertions sont isolés du raccourci M et du commit de
|
||||
livraison contenant les versions et la documentation : `git revert 5ea7950` annule le
|
||||
calque et sa disposition. Après ajout du raccourci (`94fefc0`), l'application
|
||||
inverse du patch a été vérifiée avec `git apply --reverse --3way --cached`
|
||||
dans un index temporaire : aucun conflit et raccourci M conservé, sans modifier
|
||||
les fichiers de travail. La branche
|
||||
`codex/pause-columns-beta235` conserve la variante précédente. Annuler le
|
||||
commit de disposition ramène à cette variante ; une livraison suivante prend
|
||||
le prochain numéro beta, sans diminuer le compteur.
|
||||
|
||||
## Vérifications
|
||||
|
||||
```sh
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryStory234ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
|
||||
./gradlew :sanctuary:runClientGameTest -PsanctuaryClientTests=true -PsanctuaryPause233ClientTests=true -PsanctuaryClientGraphicsBackend=vulkan
|
||||
./gradlew check build assemblePack -PsanctuaryFocusedTests=menus -PsanctuaryAtlasOnly=true
|
||||
```
|
||||
|
||||
Parcours Story et raccourci réussi en 42 s (`STORY234_PASS`) : Vulkan FR/EN GUI 2/3/4,
|
||||
fenêtre 1280 × 960, avec et sans Atlas, puis redimensionnement à 1344 × 1008.
|
||||
Nord centré sous le message serveur, terrain réellement reçu, calque aux
|
||||
dimensions de l'écran, boutons et titres des fils prioritaires pour les clics
|
||||
et le survol. Clic natif sur une zone libre et Entrée ouvrent l'Atlas ; Échap
|
||||
et Terminé reviennent à la même Pause. Le clic natif sur Story traverse bien
|
||||
le premier plan, ses 100 entrées, recherche et filtres restent disponibles.
|
||||
Saisie continue, focus, défilement, retours et progression serveur conservés.
|
||||
Événements clavier natifs M depuis le jeu et Pause avant/après déblocage,
|
||||
réaffectation à J, suppression de l'ancienne affectation, désactivation et
|
||||
restauration de la touche testées. La saisie de M dans Story garde le texte,
|
||||
le focus et son écran ; les intitulés du raccourci sont vérifiés en FR/EN.
|
||||
Log : `build/pause236-story-client-vulkan-final.log`.
|
||||
|
||||
21 captures copiées dans `build/pause236-story-screenshots/`. Captures inspectées :
|
||||
avec Atlas FR/2 et EN/4, sans Atlas FR/2. La fixture du monde plat neuf
|
||||
cartographie explicitement 25 chunks : la majorité du fond reste inconnue.
|
||||
Ce terrain limité ne représente pas une sauvegarde personnelle explorée.
|
||||
Aux GUI étroits, les longs libellés natifs et titres des fils gardent leur
|
||||
défilement ou découpage ; les actions Sanctuary restent lisibles en entier.
|
||||
Validation macOS/Vulkan uniquement, aucun essai OpenGL ou Windows revendiqué.
|
||||
|
||||
Le parcours Pause final réussit en 42 s (`PAUSE233_PASS`), avec les mêmes
|
||||
langues, GUI et états Atlas. Les fils restent de part et d'autre du menu ;
|
||||
publication, photo, retours des sélecteurs, brouillons et ancienne catégorie
|
||||
sont conservés. Inventaire réel et fichier communautaire restent inchangés
|
||||
pendant les lectures ; configuration temporaire restaurée à la fin.
|
||||
Log : `build/pause236-client-vulkan-final.log` ; 38 captures copiées dans
|
||||
`build/pause236-screenshots/`. Captures inspectées : Pause avec Atlas et posts
|
||||
FR/2 et EN/4. Vulkan confirmé : MoltenVK 1.4.2, Apple M1.
|
||||
|
||||
Le contrôle `check build assemblePack` réussit en 2 min 31 s : quatre GameTests
|
||||
requis et contrôles purs du dépôt, dont fichier communautaire, photos,
|
||||
demandes et recherche. Log : `build/pause236-check-build-pack-final.log`.
|
||||
La sélection menus/Atlas reste celle du banc existant ; le conflit du générateur
|
||||
sur le chemin natif par défaut, documenté en beta.232, reste hors de ce ticket.
|
||||
La compilation des sources finales du banc client passe également.
|
||||
|
||||
## Livraison locale
|
||||
|
||||
`mod_version`, `pack_version` et `packwiz/pack.toml` sont alignés sur beta.236.
|
||||
Les commits `5ea7950` (disposition) et `94fefc0` (raccourci) restent séparés
|
||||
des métadonnées de livraison.
|
||||
|
||||
Export historique [Sanctuary-beta.236.mrpack](../build/withdrawn/Sanctuary-beta.236.mrpack),
|
||||
12 779 209 octets : intégrité ZIP, Minecraft 26.3 et Fabric Loader 0.19.5,
|
||||
un seul JAR Sanctuary embarqué, identique au JAR construit, sans monde ni module
|
||||
de test. Les 3 303 fichiers d'assets, données et resourcepacks sont identiques
|
||||
à beta.235, sauf un nouveau libellé de raccourci dans chaque langue ; toutes
|
||||
les traductions existantes sont conservées. Story reste dans le JAR de production
|
||||
et les classes des bancs client en sont absentes.
|
||||
Reçu : `build/pause236-mrpack-receipt.json`.
|
||||
|
||||
- JAR : `74a8be9a078e9ecdc17b8cf332254cdb4a6dca4c37e4fb728d98632d2e8c3918`.
|
||||
- MRpack : `0fac46765a4430df8f90398471dedd2c2636d6b120482cc78466011fdd7574c2`.
|
||||
|
||||
Aucune publication distante, modification du canal packwiz, synchronisation
|
||||
Prism ou ouverture d'un monde personnel effectuée.
|
||||
+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.236
|
||||
pack_version=beta.236
|
||||
mod_version=beta.238
|
||||
pack_version=beta.238
|
||||
resource_pack_version=beta.121
|
||||
maven_group=fr.koka.sanctuary
|
||||
jei_version=30.32.0-sanctuary.3
|
||||
|
||||
@@ -228,7 +228,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 = [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 = [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)
|
||||
@@ -337,6 +337,9 @@ tasks.named('processGametestResources') {
|
||||
def pause233Client = providers.gradleProperty('sanctuaryPause233ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryPause233ClientTests', pause233Client)
|
||||
if (pause233Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Pause233ClientChecks') }
|
||||
def aptitudes236Client = providers.gradleProperty('sanctuaryAptitudes236ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('aptitudes236Client', aptitudes236Client)
|
||||
if (aptitudes236Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Aptitudes236ClientChecks') }
|
||||
def story234Client = providers.gradleProperty('sanctuaryStory234ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('sanctuaryStory234ClientTests', story234Client)
|
||||
if (story234Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Story234ClientChecks') }
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.recipes.RecipeService;
|
||||
import fr.koka.sanctuary.food.FoodKnowledgeService;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import java.nio.file.*;
|
||||
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.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import static fr.koka.sanctuary.client.Community154ClientChecks.*;
|
||||
|
||||
/** Native Progression widgets and real server requests in a disposable Vulkan world. */
|
||||
public final class Aptitudes236ClientChecks implements FabricClientGameTest {
|
||||
private static final Map<String,String> TITLES=Map.ofEntries(
|
||||
Map.entry("names","sanctuary.progression.names"),Map.entry("atlas","sanctuary.progression.atlas"),
|
||||
Map.entry("sorting","sanctuary.progression.sorting"),Map.entry("swimming","sanctuary.progression.swimming"),
|
||||
Map.entry("resting","sanctuary.progression.resting"),Map.entry("active_bond","sanctuary.companion.link"),
|
||||
Map.entry("familiar_study","sanctuary.companion.study"),Map.entry("catalogue","sanctuary.recipes.catalogue"),
|
||||
Map.entry("zoom","sanctuary.zoom.title"),Map.entry("enchanting","sanctuary.progression.enchanting"),Map.entry("alchemy","sanctuary.progression.alchemy"),Map.entry("smithing","sanctuary.progression.smithing"));
|
||||
@Override public void runTest(ClientGameTestContext c) {
|
||||
var intro=new AtomicBoolean(true);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("Forgerie238 "+UUID.randomUUID());s.setSeed("238");s.setAllowCommands(false);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("minecraft:flat")).findFirst().orElseThrow());}).create()) {
|
||||
c.waitFor(m->ProgressionClient.active() && m.player!=null && 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.setExperienceLevels(100);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->m.player.experienceLevel==100,200);
|
||||
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));check(!CraftAptitudes.learned(m.player,"enchanting") && !CraftAptitudes.learned(m.player,"alchemy") && !CraftAptitudes.learned(m.player,"smithing"),"New workstations start locked");});
|
||||
for(String craft:List.of("enchanting","alchemy","smithing")) {
|
||||
c.runOnClient(m->{var button=afterTitle(m.gui.screen(),TITLES.get(craft));check(key(button).equals("sanctuary.progression.unlock_cost")&&button.active,"Locked craft offers its normal purchase button");button.onPress(enter());});
|
||||
c.waitFor(m->CraftAptitudes.learned(m.player,craft) && key(afterTitle(m.gui.screen(),TITLES.get(craft))).equals("sanctuary.progression.aptitude_acquired"),200);
|
||||
}
|
||||
c.runOnClient(m->check(m.player.experienceLevel==88,"Three UI purchases cost twelve levels"));
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setExperienceLevels(100);
|
||||
for(String target:ProgressRecord.APTITUDES)check(ProgressionService.purchase(p,target,ProgressEffects.get(p).revision()),"Server acquires "+target);
|
||||
check(CompanionService.buy(p),"Active Bond bought");check(CompanionStudy.buy(p),"Familiar Study bought");
|
||||
check(RecipeService.buy(p),"Catalogue bought");check(ZoomService.buy(p),"Zoom bought");check(FoodKnowledgeService.buy(p),"Food Knowledge bought");
|
||||
CompanionService.send(p,"");ProgressionService.send(p,"");
|
||||
});
|
||||
c.runOnClient(m->m.gui.setScreen(null));
|
||||
c.waitFor(m->ProgressionClient.progress.names() && CompanionClient.studied && ZoomClient.learned() && RecipeClient.knowledge!=null && RecipeClient.knowledge.learned() && FoodKnowledgeClient.ready(),300);
|
||||
c.waitTicks(30);
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.setAttached(AptitudeService.PREFERENCES,2044);ProgressionService.send(p,"");check(p.getAttached(AptitudeService.PREFERENCES)==0,"Legacy preferences are normalized without losing purchases");});
|
||||
c.runOnClient(m->{m.player.setAttached(AptitudeService.PREFERENCES,2044);for(String target:AptitudePreferences.PERMANENT)check(AptitudeClient.enabled(target),"Client ignores legacy disabled bits for "+target);});
|
||||
String purchaseProof=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD));
|
||||
int xp=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().experienceLevel);
|
||||
for(String language:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));});
|
||||
c.waitTicks(3);
|
||||
c.runOnClient(m->{var screen=m.gui.screen();check(screen.isPauseScreen(),"Progression retains Pause semantics");layout(screen);var titles=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).map(Aptitudes236ClientChecks::key).toList();
|
||||
int previous=-1;for(String theme:List.of("survival","inventory_crafting","familiars","exploration","crafts")){int index=titles.indexOf("sanctuary.progression.theme_"+theme);check(index>previous,"All five themes are present in order");previous=index;}
|
||||
check(has(screen,"sanctuary.progression.aptitude_permanent"),"Food Knowledge is visibly permanent");
|
||||
check(key(afterTitle(screen,"sanctuary.food_knowledge.title")).equals("sanctuary.food_knowledge.learned") && !afterTitle(screen,"sanctuary.food_knowledge.title").active,"Food Knowledge has no activation switch");
|
||||
for(var entry:TITLES.entrySet()){var button=afterTitle(screen,entry.getValue());boolean toggle=AptitudePreferences.TOGGLEABLE.contains(entry.getKey());
|
||||
check(key(button).equals(toggle?"sanctuary.progression.aptitude_enabled":"sanctuary.progression.aptitude_acquired") && button.active==toggle,"Only World Map and Mob Names have switches: "+entry.getKey());check(m.font.width(button.getMessage())+8<=button.getWidth(),"Aptitude label fits without clipping");}
|
||||
});
|
||||
shot(c,"top-"+language+"-"+scale);
|
||||
for(String target:AptitudePreferences.TOGGLEABLE) {
|
||||
c.runOnClient(m->{var screen=m.gui.screen();var button=afterTitle(screen,TITLES.get(target));reveal(screen,button);check(button.active,"Toggle is ready: "+target);button.onPress(enter());});
|
||||
c.waitFor(m->!AptitudeClient.enabled(target) && key(afterTitle(m.gui.screen(),TITLES.get(target))).equals("sanctuary.progression.aptitude_disabled"),200);
|
||||
check(server.computeOnServer(s->!AptitudeService.enabled(s.getPlayerList().getPlayers().getFirst(),target)),"UI choice is authoritative: "+target);
|
||||
if(target.equals("atlas")) {
|
||||
c.runOnClient(m->{m.gui.setScreen(new PauseScreen(true));pauseLayout(m.gui.screen(),false);});shot(c,"pause-map-disabled-"+language+"-"+scale);
|
||||
c.runOnClient(m->m.gui.setScreen(new ProgressionScreen(new PauseScreen(true))));
|
||||
}
|
||||
c.runOnClient(m->afterTitle(m.gui.screen(),TITLES.get(target)).onPress(enter()));
|
||||
c.waitFor(m->AptitudeClient.enabled(target) && key(afterTitle(m.gui.screen(),TITLES.get(target))).equals("sanctuary.progression.aptitude_enabled"),200);
|
||||
}
|
||||
c.runOnClient(m->{m.gui.setScreen(new PauseScreen(true));pauseLayout(m.gui.screen(),true);});shot(c,"pause-map-enabled-"+language+"-"+scale);
|
||||
c.runOnClient(m->{m.gui.setScreen(new ProgressionScreen(new PauseScreen(true)));reveal(m.gui.screen(),afterTitle(m.gui.screen(),TITLES.get("smithing")));});
|
||||
shot(c,"crafts-"+language+"-"+scale);
|
||||
c.runOnClient(m->{var screen=m.gui.screen();var area=aptitudes(screen);area.setScrollAmount(100);});
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1344,1008);m.resizeGui();});c.waitTicks(3);
|
||||
c.runOnClient(m->{check(aptitudes(m.gui.screen()).scrollAmount()==100,"Resize retains aptitude scroll");layout(m.gui.screen());press(m.gui.screen(),"gui.done");check(m.gui.screen() instanceof PauseScreen,"Done returns to Pause");m.gui.setScreen(null);});
|
||||
}
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();check(p.experienceLevel==xp && p.getAttached(ProgressionService.RECORD).equals(purchaseProof),"Twelve disable/reactivate pairs preserve XP and purchases");});
|
||||
System.out.println("FORGERIE238_PASS: Vulkan FR/EN GUI 2/3/4, five themes, two real server toggles, eleven permanent purchases, legacy preferences and merged Pause layout, three native UI purchases, preserved XP/purchases, scroll/resize and Done");
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
private static void pauseLayout(Screen screen,boolean enabled) {Pause233ClientChecks.pause(screen,enabled);}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static Button afterTitle(Screen screen,String title) {
|
||||
var list=widgets(screen).filter(AbstractWidget.class::isInstance).map(AbstractWidget.class::cast).toList();
|
||||
int index=java.util.stream.IntStream.range(0,list.size()).filter(i->key(list.get(i)).equals(title)).findFirst().orElseThrow(()->new AssertionError("Missing title "+title));
|
||||
return list.subList(index+1,list.size()).stream().filter(Button.class::isInstance).map(Button.class::cast).findFirst().orElseThrow();
|
||||
}
|
||||
private static AbstractScrollArea aptitudes(Screen screen){return widgets(screen).filter(AbstractScrollArea.class::isInstance).map(AbstractScrollArea.class::cast).max(Comparator.comparingInt(AbstractWidget::getX)).orElseThrow();}
|
||||
private static void reveal(Screen screen,AbstractWidget widget){var area=aptitudes(screen);area.setScrollAmount(Math.clamp(area.scrollAmount()+widget.getBottom()-area.getBottom()+4,0,area.maxScrollAmount()));}
|
||||
private static void layout(Screen screen){var area=aptitudes(screen);check(area.getX()>=0&&area.getRight()<=screen.width&&area.getBottom()<screen.height-26,"Aptitudes viewport stays inside native frame");for(var button:widgets(screen).filter(Button.class::isInstance).map(Button.class::cast).toList())check(button.getX()>=0&&button.getRight()<=screen.width,"Buttons remain within the screen horizontally");}
|
||||
private static void shot(ClientGameTestContext c,String name){var source=c.takeScreenshot("forgerie238-"+name);try{var dir=FabricLoader.getInstance().getGameDir().resolve("../../../../../build/forgerie238-screenshots").normalize();Files.createDirectories(dir);Files.copy(source,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);}catch(Exception e){throw new AssertionError(e);}}
|
||||
}
|
||||
-35
@@ -10,7 +10,6 @@ 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.KeyMapping;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.*;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
@@ -38,7 +37,6 @@ public final class Story234ClientChecks implements FabricClientGameTest {
|
||||
server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var r=ProgressRecord.INITIAL;if(unlocked)r=r.buy("atlas",r.cost("atlas",ProgressionService.session(s).config));p.setAttached(ProgressionService.RECORD,r.json());ProgressEffects.apply(p,r);ProgressionService.send(p,"");});
|
||||
c.waitFor(m->ProgressionClient.progress.atlas()==unlocked,200);
|
||||
String before=server.computeOnServer(s->s.getPlayerList().getPlayers().getFirst().getAttached(ProgressionService.RECORD));
|
||||
shortcut(c,unlocked);
|
||||
for(String language:List.of("fr_fr","en_us"))for(int scale:List.of(2,3,4)) {
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1280,960);m.options.guiScale().set(scale);m.resizeGui();m.getLanguageManager().setSelected(language);m.getLanguageManager().onResourceManagerReload(m.getResourceManager());m.gui.setScreen(new PauseScreen(true));});
|
||||
c.waitTicks(3);
|
||||
@@ -46,10 +44,6 @@ public final class Story234ClientChecks implements FabricClientGameTest {
|
||||
shot(c,"pause-"+unlocked+"-"+language+"-"+scale);
|
||||
c.runOnClient(m->{var pause=m.gui.screen();press(pause,"sanctuary.story.title");check(m.gui.screen() instanceof StoryScreen,"Story opens from Pause without Atlas gating");check(m.gui.screen().isPauseScreen(),"Story retains solo pause");check(m.gui.screen().getTitle().getString().equals("Story"),"Story has its own accessible screen title");check(edit(m.gui.screen()).getValue().isEmpty()&&cycle(m.gui.screen()).getValue().toString().equals("ALL"),"Empty search and All on opening");assertEntries(m.gui.screen(),100);layout(m.gui.screen());});
|
||||
shot(c,"story-"+language+"-"+scale);
|
||||
c.runOnClient(m->{var s=m.gui.screen();var search=edit(s);s.setFocused(search);
|
||||
s.keyPressed(new KeyEvent(mapKey().getDefaultKey().getValue(),InputConstants.KEYCODE_M,0));
|
||||
check(s.charTyped(new CharacterEvent('m'))&&m.gui.screen()==s&&search.getValue().equals("m")&&search.isFocused(),"Typing M in Story keeps its screen, text and focus");
|
||||
check(net.minecraft.network.chat.Component.translatable("sanctuary.atlas.key").getString().equals(language.equals("fr_fr")?"Ouvrir la carte":"Open world map"),"Map shortcut has a localized Controls label");search.setValue("");});
|
||||
c.runOnClient(m->{var s=m.gui.screen();var search=edit(s);s.setFocused(search);for(int codepoint:"100".codePoints().toArray()){check(s.charTyped(new CharacterEvent(codepoint)),"Native typing handled");check(s.getFocused()==search&&search.isFocused(),"List refresh retains search focus");}check(search.getValue().equals("100")&&search.getCursorPosition()==3,"Continuous typing retains cursor");assertEntries(s,1);check(entries(s).getFirst().getMessage().getString().equals(StoryScreen.label("unknown_entry",100).getString()),"Search selects story 100");search.setValue(" 100 ");assertEntries(s,1);cycle(s).onPress(enter());check(has(s,"sanctuary.story.no_discoveries")&&!has(s,"sanctuary.story.no_results")&&entries(s).isEmpty(),"Discovered has a distinct empty state");cycle(s).onPress(enter());check(cycle(s).getValue().toString().equals("UNKNOWN"),"Third filter is Unknown");assertEntries(s,1);search.setValue("absent-story");check(has(s,"sanctuary.story.no_results")&&!has(s,"sanctuary.story.no_discoveries"),"Search empty state is distinct");search.setValue(language.equals("fr_fr")?"INCONNUE":"UNKNOWN");assertEntries(s,100);var area=area(s);check(s.mouseScrolled(area.getX()+5,area.getY()+5,0,-5)&&area.scrollAmount()>0,"Native wheel scrolls Story");area.setScrollAmount(area.maxScrollAmount());check(entries(s).getLast().getBottom()<=area.getBottom(),"Story 100 can be reached at the end");area.setScrollAmount(100);});
|
||||
c.runOnClient(m->{m.getWindow().setWindowed(1344,1008);m.resizeGui();});c.waitTicks(3);
|
||||
c.runOnClient(m->{var s=m.gui.screen();check(edit(s).getValue().equals(language.equals("fr_fr")?"INCONNUE":"UNKNOWN")&&cycle(s).getValue().toString().equals("UNKNOWN"),"Resize retains query and filter");check(area(s).scrollAmount()==100,"Resize retains scroll position");layout(s);edit(s).setValue("100");check(area(s).scrollAmount()==0,"Search resets scroll");edit(s).setValue("");area(s).setScrollAmount(100);cycle(s).onPress(enter());check(cycle(s).getValue().toString().equals("ALL")&&area(s).scrollAmount()==0,"Filter resets scroll and cycles back to All");assertEntries(s,100);});
|
||||
@@ -68,35 +62,6 @@ public final class Story234ClientChecks implements FabricClientGameTest {
|
||||
}finally{intro.set(false);}
|
||||
}
|
||||
|
||||
private static KeyMapping mapKey(){return KeyMapping.get("sanctuary.atlas.key");}
|
||||
private static void shortcut(ClientGameTestContext c,boolean unlocked) {
|
||||
var saved=c.computeOnClient(m->InputConstants.getKey(mapKey().saveString()));
|
||||
int scan=c.computeOnClient(m->{var key=mapKey();check(key!=null,"Map is registered in native Controls");
|
||||
int nativeScan=org.lwjgl.sdl.SDLKeyboard.SDL_GetScancodeFromKey(InputConstants.KEYCODE_M,(java.nio.ShortBuffer)null);
|
||||
check(nativeScan>0&&key.getDefaultKey().getValue()==nativeScan,"Default follows the keyboard layout's M letter");
|
||||
key.setKey(key.getDefaultKey());KeyMapping.resetMapping();m.gui.setScreen(null);return nativeScan;});
|
||||
try {
|
||||
c.getInput().pressKey(scan);c.waitTicks(5);
|
||||
c.runOnClient(m->{check(unlocked?m.gui.screen() instanceof SanctuaryAtlasScreen:m.gui.screen()==null,"Game M respects Atlas unlock");});
|
||||
if(unlocked){c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen()==null,"Atlas opened from game returns to game"));}
|
||||
var pause=c.computeOnClient(m->{var screen=new PauseScreen(true);m.gui.setScreen(screen);return screen;});
|
||||
c.getInput().pressKey(scan);c.waitTicks(3);
|
||||
c.runOnClient(m->{check(unlocked?m.gui.screen() instanceof SanctuaryAtlasScreen:m.gui.screen()==pause,"Pause M respects Atlas unlock");
|
||||
if(unlocked){press(m.gui.screen(),"gui.done");check(m.gui.screen()==pause,"M from Pause keeps original parent");}m.gui.setScreen(null);});
|
||||
if(unlocked) {
|
||||
c.runOnClient(m->{mapKey().setKey(InputConstants.Type.KEYBOARD.getOrCreate(InputConstants.KEY_J));KeyMapping.resetMapping();});
|
||||
c.getInput().pressKey(scan);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen()==null,"Old M binding stops opening the map"));
|
||||
c.getInput().pressKey(InputConstants.KEY_J);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen() instanceof SanctuaryAtlasScreen,"Remapped J opens map from game"));
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);
|
||||
c.runOnClient(m->m.gui.setScreen(pause));c.getInput().pressKey(scan);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen()==pause,"Pause respects removed M binding"));
|
||||
c.getInput().pressKey(InputConstants.KEY_J);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen() instanceof SanctuaryAtlasScreen,"Pause respects remapped J"));
|
||||
c.getInput().pressKey(InputConstants.KEY_ESCAPE);c.waitTicks(3);c.runOnClient(m->{check(m.gui.screen()==pause,"Remapped shortcut retains Pause parent");mapKey().setKey(InputConstants.UNKNOWN);KeyMapping.resetMapping();});
|
||||
c.getInput().pressKey(scan);c.getInput().pressKey(InputConstants.KEY_J);c.waitTicks(3);c.runOnClient(m->check(m.gui.screen()==pause,"Unbound disables map shortcut"));
|
||||
}
|
||||
System.out.println("ATLAS236_SHORTCUT_PASS: native layout-aware M, game/Pause returns, unlock="+unlocked+", configurable key and Unbound, binding restored");
|
||||
}finally{c.getInput().releaseKey(scan);c.getInput().releaseKey(InputConstants.KEY_J);c.runOnClient(m->{m.gui.setScreen(null);mapKey().setKey(saved);KeyMapping.resetMapping();});}
|
||||
}
|
||||
|
||||
private static List<Button> entries(Screen s){return widgets(s).filter(Button.class::isInstance).map(Button.class::cast).filter(b->key(b).equals("sanctuary.story.unknown_entry")).toList();}
|
||||
private static String key(AbstractWidget w){return w.getMessage().getContents() instanceof TranslatableContents t?t.getKey():"";}
|
||||
private static CycleButton<?> cycle(Screen s){return widgets(s).filter(CycleButton.class::isInstance).map(CycleButton.class::cast).findFirst().orElseThrow();}
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.companion.*;
|
||||
import fr.koka.sanctuary.recipes.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.atlas.AtlasService;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Behavioral checks on real server players, native menus and vanilla persistence. */
|
||||
public final class Aptitudes236GameTests {
|
||||
@GameTest public void preferencesPreservePurchasesXpDeathAndCycle(GameTestHelper h) {
|
||||
var record=ProgressRecord.INITIAL;
|
||||
for(String skill:ProgressRecord.SKILLS)while(record.rank(skill)<ProgressRecord.cap(skill))record=record.buy(skill,record.cost(skill,ProgressionConfig.DEFAULT));
|
||||
for(String aptitude:ProgressRecord.APTITUDES)record=record.buy(aptitude,4);
|
||||
var p=player(h,record);p.setExperienceLevels(20);
|
||||
p.setAttached(CompanionService.RECORD,CompanionRecord.EMPTY.purchase(4,1).json());
|
||||
p.setAttached(CompanionStudy.RECEIPT,"{\"schema\":1,\"paid\":4,\"at\":1}");
|
||||
p.setAttached(ZoomService.LEARNED,true);
|
||||
h.assertTrue(RecipeService.buy(p),"Catalogue acquired through its normal server purchase");
|
||||
h.assertTrue(CraftAptitudes.buy(p,"enchanting",record.revision()) && CraftAptitudes.buy(p,"alchemy",record.revision()),"New craft purchases accepted");
|
||||
String proof=p.getAttachedOrThrow(ProgressionService.RECORD),recipes=p.getAttached(RecipeService.RECORD),bond=p.getAttached(CompanionService.RECORD);
|
||||
int xp=p.experienceLevel;
|
||||
h.assertTrue(!p.hasAttached(AptitudeService.PREFERENCES),"Old players need no preference migration");
|
||||
for(String target:AptitudePreferences.TOGGLEABLE)h.assertTrue(toggle(p,target,false),"Owned aptitude can be disabled: "+target);
|
||||
h.assertTrue(AptitudeService.preferences(p).disabledMask()==AptitudePreferences.ACTIVE_MASK && p.experienceLevel==xp,"Both display choices are independent and free");
|
||||
h.assertTrue(proof.equals(p.getAttached(ProgressionService.RECORD)) && recipes.equals(p.getAttached(RecipeService.RECORD)) && bond.equals(p.getAttached(CompanionService.RECORD)),"Purchase proofs remain byte-for-byte unchanged");
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
|
||||
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
|
||||
h.assertTrue(AptitudeService.preferences(loaded).equals(AptitudeService.preferences(p)) && CraftAptitudes.learned(loaded,"enchanting") && CraftAptitudes.learned(loaded,"alchemy"),"Choices and craft purchases reload from vanilla player NBT");
|
||||
var respawn=Progression003GameTests.player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(p,false);
|
||||
ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(p,respawn,false);
|
||||
h.assertTrue(AptitudeService.preferences(respawn).equals(AptitudeService.preferences(p)) && CraftAptitudes.learned(respawn,"alchemy"),"Choices and new purchases survive ordinary death");
|
||||
bind(p,record.nextCycle());
|
||||
h.assertTrue(AptitudeService.preferences(p).disabledMask()==AptitudePreferences.ACTIVE_MASK && CraftAptitudes.learned(p,"enchanting"),"Skill reset preserves separate preferences and craft purchases");
|
||||
for(String target:AptitudePreferences.TOGGLEABLE)h.assertTrue(toggle(p,target,true),"Reactivation needs no new purchase: "+target);
|
||||
h.assertTrue(p.experienceLevel==xp && AptitudeService.preferences(p).equals(AptitudePreferences.DEFAULT),"Reactivation costs no XP");h.succeed();
|
||||
}
|
||||
@GameTest public void unknownUnownedPermanentAndStaleRequestsRefused(GameTestHelper h) {
|
||||
var p=player(h,ProgressRecord.INITIAL.buy("names",4));p.setExperienceLevels(10);
|
||||
for(String target:new String[]{"swimming","enchanting","alchemy","food_knowledge","unknown","vitality"})
|
||||
h.assertTrue(!toggle(p,target,false),"Cannot toggle unowned, permanent or non-aptitude target: "+target);
|
||||
h.assertTrue(toggle(p,"names",false),"Owned name reveal disables");
|
||||
h.assertTrue(!AptitudeService.set(p,new AptitudeService.Set("names",true,0)),"Stale mask cannot overwrite a newer choice");
|
||||
h.assertTrue(!CraftAptitudes.buy(p,"enchanting",99) && !CraftAptitudes.buy(p,"unknown",1),"Stale and unknown purchases refused");
|
||||
p.setExperienceLevels(3);h.assertTrue(!CraftAptitudes.buy(p,"alchemy",1) && p.experienceLevel==3,"Insufficient XP never spent");
|
||||
p.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(p,"alchemy",1) && p.experienceLevel==0 && !CraftAptitudes.buy(p,"alchemy",1),"Four levels buy once");
|
||||
h.assertTrue(!AptitudeService.enabled(p,"names"),"Refusal preserves previous choice");h.succeed();
|
||||
}
|
||||
@GameTest public void legacyPreferencesRestorePermanentPurchases(GameTestHelper h) {
|
||||
var p=player(h,ProgressRecord.INITIAL.buy("names",4).buy("atlas",4));p.setExperienceLevels(12);
|
||||
h.assertTrue(CraftAptitudes.buy(p,"enchanting",2) && CraftAptitudes.buy(p,"alchemy",2),"Craft purchases exist before legacy migration");
|
||||
p.setAttached(AptitudeService.PREFERENCES,AptitudePreferences.MAX_MASK);
|
||||
String proof=p.getAttached(ProgressionService.RECORD);int xp=p.experienceLevel;
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
|
||||
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
|
||||
h.assertTrue(loaded.getAttachedOrThrow(AptitudeService.PREFERENCES)==2047,"Historical codec still reloads all eleven bits");
|
||||
h.assertTrue(AptitudeService.preferences(loaded).disabledMask()==3 && !AptitudeService.enabled(loaded,"names") && !AptitudeService.enabled(loaded,"atlas"),"Legacy display choices retain their original bit positions");
|
||||
for(String target:AptitudePreferences.PERMANENT) {
|
||||
h.assertTrue(AptitudeService.enabled(loaded,target),"Legacy permanent preference has no effect: "+target);
|
||||
h.assertTrue(!toggle(p,target,false),"Permanent aptitude cannot be disabled: "+target);
|
||||
}
|
||||
AptitudeService.send(loaded);
|
||||
h.assertTrue(loaded.getAttachedOrThrow(AptitudeService.PREFERENCES)==3 && loaded.experienceLevel==xp && proof.equals(loaded.getAttached(ProgressionService.RECORD)),"Sync normalizes only preferences, preserving XP and purchase proofs");
|
||||
h.assertTrue(CraftAptitudes.learned(loaded,"enchanting") && CraftAptitudes.learned(loaded,"alchemy"),"Migration retains both craft purchases");h.succeed();
|
||||
}
|
||||
@GameTest public void permanentServerEffectsIgnoreLegacyDisabledBits(GameTestHelper h) {
|
||||
var r=ProgressRecord.INITIAL.buy("hunger",1).buy("swimming",4).buy("resting",4).buy("sorting",4).buy("atlas",4);
|
||||
var p=player(h,r);p.getFoodData().setFoodLevel(8);p.setAttached(AptitudeService.PREFERENCES,2044);
|
||||
var at=p.blockPosition();for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int y=0;y<=3;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),Blocks.WATER.defaultBlockState());
|
||||
p.baseTick();p.baseTick();p.setSprinting(true);p.setSwimming(true);
|
||||
h.assertTrue(p.isSwimming() && !toggle(p,"swimming",false) && p.isSwimming() && ProgressEffects.canSwim(p),"Swimming stays usable with an old disabled bit and a rejected request");
|
||||
for(int x=-1;x<=1;x++)for(int z=-1;z<=1;z++)for(int y=0;y<=3;y++)h.getLevel().setBlockAndUpdate(at.offset(x,y,z),Blocks.AIR.defaultBlockState());
|
||||
p.baseTick();p.baseTick();p.setOnGround(true);
|
||||
h.assertTrue(LyingService.request(p,new LyingService.Request(2,r.revision())),"Purchased Rest ignores its legacy disabled bit");
|
||||
h.assertTrue(!toggle(p,"resting",false) && LyingService.posture(p)==2,"Refused disable request never stops Rest");
|
||||
h.assertTrue(InventorySortService.sort(p,new InventorySortPayloads.Sort(1,p.containerMenu.containerId,p.containerMenu.getStateId(),r.revision(),InventorySortPayloads.HOTBAR)),"Purchased inventory sorting ignores its legacy disabled bit");
|
||||
h.assertTrue(AtlasService.canRead(p,false) && toggle(p,"atlas",false) && !AtlasService.canRead(p,false),"Personal World Map access follows the display choice");
|
||||
h.assertTrue(toggle(p,"atlas",true) && AtlasService.canRead(p,false),"World Map reactivates with its existing purchase");
|
||||
LyingService.request(p,new LyingService.Request(0,r.revision()));
|
||||
p.setAttached(CompanionService.RECORD,CompanionRecord.EMPTY.purchase(4,1).json());
|
||||
h.assertTrue(!toggle(p,"active_bond",false) && CompanionService.activate(p,-1).equals("no_familiar"),"Active Bond reaches the normal familiar requirement despite its legacy disabled bit");h.succeed();
|
||||
}
|
||||
@GameTest public void enchantingNativeMenuRequiresPermanentPurchase(GameTestHelper h) {
|
||||
var p=player(h,ProgressRecord.INITIAL);p.setAttached(AptitudeService.PREFERENCES,2044);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at,Blocks.ENCHANTING_TABLE.defaultBlockState());
|
||||
var provider=new SimpleMenuProvider((id,inventory,player)->new EnchantmentMenu(id,inventory,ContainerLevelAccess.create(h.getLevel(),at)),Component.literal("Enchanting"));
|
||||
h.assertTrue(p.openMenu(provider).isEmpty() && p.containerMenu==p.inventoryMenu,"Unlearned player cannot open a native enchanting menu");
|
||||
p.setExperienceLevels(40);h.assertTrue(CraftAptitudes.buy(p,"enchanting",0) && p.experienceLevel==36,"Unlock charges exactly four levels");
|
||||
h.assertTrue(p.openMenu(provider).isPresent(),"Native menu opens after purchase");var menu=(EnchantmentMenu)p.containerMenu;
|
||||
menu.getSlot(0).set(new ItemStack(Items.DIAMOND_SWORD));menu.getSlot(1).set(new ItemStack(Items.LAPIS_LAZULI,3));
|
||||
h.assertTrue(menu.clickMenuButton(p,0) && !menu.getSlot(0).getItem().get(DataComponents.ENCHANTMENTS).isEmpty(),"Real vanilla enchanting remains functional");
|
||||
int xp=p.experienceLevel;
|
||||
h.assertTrue(!toggle(p,"enchanting",false) && p.containerMenu==menu && menu.stillValid(p) && p.experienceLevel==xp,"Purchased enchanting cannot be disabled or closed by a toggle request");
|
||||
p.closeContainer();h.assertTrue(p.openMenu(provider).isPresent(),"Enchanting remains available on later visits");p.closeContainer();h.succeed();
|
||||
}
|
||||
@GameTest public void alchemyNativeMenuRetainsSharedInventory(GameTestHelper h) {
|
||||
var p=player(h,ProgressRecord.INITIAL);p.setAttached(AptitudeService.PREFERENCES,2044);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at,Blocks.BREWING_STAND.defaultBlockState());
|
||||
var stand=(net.minecraft.world.level.block.entity.BrewingStandBlockEntity)h.getLevel().getBlockEntity(at);
|
||||
h.assertTrue(p.openMenu(stand).isEmpty(),"Unlearned player cannot open brewing stand");
|
||||
p.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(p,"alchemy",0) && p.openMenu(stand).isPresent(),"Alchemy unlock opens the native stand");
|
||||
stand.setItem(3,new ItemStack(Items.NETHER_WART));stand.setItem(4,new ItemStack(Items.BLAZE_POWDER));
|
||||
var menu=p.containerMenu;h.assertTrue(!toggle(p,"alchemy",false) && p.containerMenu==menu && menu.stillValid(p),"Alchemy cannot be disabled or closed by a toggle request");
|
||||
h.assertTrue(stand.getItem(3).is(Items.NETHER_WART) && stand.getItem(4).is(Items.BLAZE_POWDER),"Shared ingredients are never removed");
|
||||
p.closeContainer();h.assertTrue(p.openMenu(stand).isPresent(),"Purchased Alchemy stays available on later visits");
|
||||
p.closeContainer();var vanilla=Progression003GameTests.player(h);
|
||||
h.assertTrue(CraftAptitudes.allowed(vanilla,"enchanting") && CraftAptitudes.allowed(vanilla,"alchemy"),"Inactive vanilla players retain native access");h.succeed();
|
||||
}
|
||||
private static boolean toggle(ServerPlayer p,String target,boolean enabled) {return AptitudeService.set(p,new AptitudeService.Set(target,enabled,AptitudeService.preferences(p).disabledMask()));}
|
||||
private static ServerPlayer player(GameTestHelper h,ProgressRecord record) {
|
||||
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());
|
||||
try {var identities=ProgressionService.session(h.getLevel().getServer()).inhabitants;identities.create(p.getUUID(),"apt"+p.getUUID().toString().substring(0,8),"",identities.available().getFirst());}
|
||||
catch(Exception e){throw new AssertionError(e);}
|
||||
bind(p,record);return p;
|
||||
}
|
||||
private static void bind(ServerPlayer p,ProgressRecord record){p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package fr.koka.sanctuary.gametest;
|
||||
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import fr.koka.sanctuary.cosmetics.HeadBlockService;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.gametest.v1.GameTest;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.*;
|
||||
import net.minecraft.util.ProblemReporter;
|
||||
|
||||
/** Purchase persistence and real native repairs/upgrades, including worn workstations. */
|
||||
public final class Forgerie238GameTests {
|
||||
@GameTest public void purchaseIsPermanentAndSurvivesSaveDeathAndCycle(GameTestHelper h) {
|
||||
var r=ProgressRecord.INITIAL;
|
||||
for(String skill:ProgressRecord.SKILLS)while(r.rank(skill)<ProgressRecord.cap(skill))r=r.buy(skill,r.cost(skill,ProgressionConfig.DEFAULT));
|
||||
var p=player(h,r);String proof=p.getAttached(ProgressionService.RECORD);
|
||||
h.assertTrue(!p.hasAttached(CraftAptitudes.SMITHING) && !CraftAptitudes.learned(p,"smithing"),"Existing players start without the new purchase");
|
||||
p.setExperienceLevels(3);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==3,"Three levels cannot unlock Forgerie");
|
||||
p.setExperienceLevels(4);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()-1) && p.experienceLevel==4,"Stale purchase cannot spend XP");
|
||||
h.assertTrue(CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==0,"Four levels acquire Forgerie once");
|
||||
p.setExperienceLevels(8);h.assertTrue(!CraftAptitudes.buy(p,"smithing",r.revision()) && p.experienceLevel==8,"Repeat purchase costs nothing");
|
||||
h.assertTrue(!AptitudeService.set(p,new AptitudeService.Set("smithing",false,0)) && CraftAptitudes.allowed(p,"smithing"),"Bought Forgerie has no disable request");
|
||||
h.assertTrue(!CraftAptitudes.learned(p,"enchanting") && !CraftAptitudes.learned(p,"alchemy") && proof.equals(p.getAttached(ProgressionService.RECORD)),"Other purchases and progression proofs stay independent");
|
||||
var output=TagValueOutput.createWithContext(ProblemReporter.DISCARDING,h.getLevel().registryAccess());p.saveWithoutId(output);
|
||||
var loaded=Progression003GameTests.player(h);loaded.load(TagValueInput.create(ProblemReporter.DISCARDING,h.getLevel().registryAccess(),output.buildResult()));
|
||||
h.assertTrue(CraftAptitudes.learned(loaded,"smithing") && loaded.experienceLevel==8 && proof.equals(loaded.getAttached(ProgressionService.RECORD)),"Native NBT reload preserves the purchase and XP");
|
||||
var respawn=Progression003GameTests.player(h);respawn.setUUID(p.getUUID());respawn.restoreFrom(p,false);
|
||||
ServerPlayerEvents.AFTER_RESPAWN.invoker().afterRespawn(p,respawn,false);
|
||||
h.assertTrue(CraftAptitudes.learned(respawn,"smithing"),"Forgerie survives ordinary death");
|
||||
bind(p,r.nextCycle());h.assertTrue(CraftAptitudes.learned(p,"smithing") && CraftAptitudes.allowed(p,"smithing"),"Forgerie survives New Game+");h.succeed();
|
||||
}
|
||||
@GameTest public void allAnvilsRequirePurchaseThenRepairAndRename(GameTestHelper h) {
|
||||
var p=player(h,ProgressRecord.INITIAL);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at.below(),Blocks.STONE.defaultBlockState());
|
||||
for(var block:new net.minecraft.world.level.block.Block[]{Blocks.ANVIL,Blocks.CHIPPED_ANVIL,Blocks.DAMAGED_ANVIL}) {
|
||||
h.getLevel().setBlockAndUpdate(at,block.defaultBlockState());
|
||||
h.assertTrue(p.openMenu(h.getLevel().getBlockState(at).getMenuProvider(h.getLevel(),at)).isEmpty(),"Every anvil variant is locked before purchase");
|
||||
}
|
||||
p.setExperienceLevels(40);h.assertTrue(CraftAptitudes.buy(p,"smithing",0) && p.experienceLevel==36,"Anvil unlock charges four levels");
|
||||
for(var block:new net.minecraft.world.level.block.Block[]{Blocks.ANVIL,Blocks.CHIPPED_ANVIL,Blocks.DAMAGED_ANVIL}) {
|
||||
h.getLevel().setBlockAndUpdate(at,block.defaultBlockState());
|
||||
h.assertTrue(p.openMenu(h.getLevel().getBlockState(at).getMenuProvider(h.getLevel(),at)).isPresent(),"Every acquired anvil variant opens");
|
||||
var menu=(AnvilMenu)p.containerMenu;var sword=new ItemStack(Items.DIAMOND_SWORD);sword.setDamageValue(100);
|
||||
menu.getSlot(AnvilMenu.INPUT_SLOT).set(sword);menu.getSlot(AnvilMenu.ADDITIONAL_SLOT).set(new ItemStack(Items.DIAMOND));
|
||||
h.assertTrue(menu.setItemName("Forgerie238") && menu.stillValid(p),"Native anvil accepts a repair and name");
|
||||
int cost=menu.getCost(),xp=p.experienceLevel;
|
||||
var result=menu.getSlot(AnvilMenu.RESULT_SLOT).safeTake(1,1,p);
|
||||
h.assertTrue(result.is(Items.DIAMOND_SWORD) && result.getDamageValue()==0 && result.get(DataComponents.CUSTOM_NAME).getString().equals("Forgerie238"),"Native result is repaired and renamed");
|
||||
h.assertTrue(cost>0 && p.experienceLevel==xp-cost && menu.getSlot(AnvilMenu.ADDITIONAL_SLOT).getItem().isEmpty(),"Native operation consumes its normal XP and repair material");p.closeContainer();
|
||||
}
|
||||
h.succeed();
|
||||
}
|
||||
@GameTest public void smithingTableUpgradesAndTrimsAfterPurchase(GameTestHelper h) {
|
||||
var p=player(h,ProgressRecord.INITIAL);var at=p.blockPosition();h.getLevel().setBlockAndUpdate(at,Blocks.SMITHING_TABLE.defaultBlockState());
|
||||
var provider=h.getLevel().getBlockState(at).getMenuProvider(h.getLevel(),at);
|
||||
h.assertTrue(p.openMenu(provider).isEmpty(),"Smithing table is locked before purchase");
|
||||
p.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(p,"smithing",0) && p.openMenu(provider).isPresent(),"Same purchase opens smithing table");
|
||||
var menu=(SmithingMenu)p.containerMenu;
|
||||
inputs(menu,Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE,Items.DIAMOND_SWORD,Items.NETHERITE_INGOT);
|
||||
var upgrade=menu.getSlot(SmithingMenu.RESULT_SLOT).safeTake(1,1,p);
|
||||
h.assertTrue(upgrade.is(Items.NETHERITE_SWORD) && p.experienceLevel==0,"Real netherite upgrade works without extra XP");
|
||||
h.assertTrue(menu.getSlot(0).getItem().isEmpty() && menu.getSlot(1).getItem().isEmpty() && menu.getSlot(2).getItem().isEmpty(),"Upgrade consumes each native input once");
|
||||
inputs(menu,Items.SENTRY_ARMOR_TRIM_SMITHING_TEMPLATE,Items.DIAMOND_CHESTPLATE,Items.REDSTONE);
|
||||
var trimmed=menu.getSlot(SmithingMenu.RESULT_SLOT).safeTake(1,1,p);
|
||||
h.assertTrue(trimmed.is(Items.DIAMOND_CHESTPLATE) && trimmed.has(DataComponents.TRIM),"Native armor trim remains available");
|
||||
p.closeContainer();h.assertTrue(p.openMenu(provider).isPresent(),"Forgerie remains available on later visits");p.closeContainer();h.succeed();
|
||||
}
|
||||
@GameTest public void forgedMenusCannotYieldAnUnownedResult(GameTestHelper h) {
|
||||
var p=player(h,ProgressRecord.INITIAL);p.setExperienceLevels(20);var at=p.blockPosition();
|
||||
h.getLevel().setBlockAndUpdate(at,Blocks.ANVIL.defaultBlockState());
|
||||
var anvil=new AnvilMenu(7,p.getInventory(),ContainerLevelAccess.create(h.getLevel(),at));
|
||||
anvil.getSlot(0).set(new ItemStack(Items.DIAMOND_SWORD));anvil.setItemName("Forbidden");
|
||||
h.assertTrue(anvil.getSlot(AnvilMenu.RESULT_SLOT).hasItem() && !anvil.stillValid(p) && !anvil.getSlot(AnvilMenu.RESULT_SLOT).mayPickup(p),"Anvil constructed outside normal opening is still locked");
|
||||
h.assertTrue(anvil.getSlot(AnvilMenu.RESULT_SLOT).safeTake(1,1,p).isEmpty() && p.experienceLevel==20 && anvil.getSlot(0).hasItem(),"Rejected anvil take changes neither XP nor inputs");anvil.removed(p);
|
||||
h.getLevel().setBlockAndUpdate(at,Blocks.SMITHING_TABLE.defaultBlockState());
|
||||
var forge=new SmithingMenu(8,p.getInventory(),ContainerLevelAccess.create(h.getLevel(),at));inputs(forge,Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE,Items.DIAMOND_SWORD,Items.NETHERITE_INGOT);
|
||||
h.assertTrue(forge.getSlot(SmithingMenu.RESULT_SLOT).hasItem() && !forge.stillValid(p) && !forge.getSlot(SmithingMenu.RESULT_SLOT).mayPickup(p),"Smithing's inherited result gate rejects an unowned menu");
|
||||
h.assertTrue(forge.getSlot(SmithingMenu.RESULT_SLOT).safeTake(1,1,p).isEmpty() && forge.getSlot(1).hasItem(),"Rejected smithing take never consumes its base item");forge.removed(p);
|
||||
var vanilla=Progression003GameTests.player(h);h.assertTrue(CraftAptitudes.allowed(vanilla,"smithing"),"Players outside Sanctuary progression retain vanilla access");h.succeed();
|
||||
}
|
||||
@GameTest public void wornWorkstationsRespectTheVisitorsPurchase(GameTestHelper h) {
|
||||
var owner=player(h,ProgressRecord.INITIAL);var actor=player(h,ProgressRecord.INITIAL);actor.setPos(owner.position().add(1,0,0));
|
||||
Item[] items={Items.ANVIL,Items.CHIPPED_ANVIL,Items.DAMAGED_ANVIL,Items.SMITHING_TABLE};
|
||||
for(var item:items) {
|
||||
AccessoryAccess.of(owner).setItem(AccessoryContainer.HEAD,new ItemStack(item));
|
||||
HeadBlockService.use(actor,owner,false);h.assertTrue(actor.containerMenu==actor.inventoryMenu,"Worn workstation cannot bypass the visitor's locked aptitude: "+item);
|
||||
}
|
||||
actor.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(actor,"smithing",0),"Visitor purchases Forgerie");
|
||||
for(var item:items) {
|
||||
AccessoryAccess.of(owner).setItem(AccessoryContainer.HEAD,new ItemStack(item));
|
||||
h.assertTrue(HeadBlockService.use(actor,owner,false) && actor.containerMenu!=actor.inventoryMenu,"Visitor uses acquired worn workstation: "+item);
|
||||
var menu=actor.containerMenu;h.assertTrue(HeadBlockService.validMenu(menu,actor,()->menu.stillValid(actor)),"Native worn menu stays valid for learned visitor");actor.closeContainer();
|
||||
}
|
||||
AccessoryAccess.of(owner).removeItemNoUpdate(AccessoryContainer.HEAD);h.succeed();
|
||||
}
|
||||
private static void inputs(SmithingMenu menu,Item template,Item base,Item addition) {menu.getSlot(0).set(new ItemStack(template));menu.getSlot(1).set(new ItemStack(base));menu.getSlot(2).set(new ItemStack(addition));}
|
||||
private static ServerPlayer player(GameTestHelper h,ProgressRecord record) {
|
||||
var p=Progression003GameTests.player(h);p.setUUID(UUID.randomUUID());
|
||||
try {var identities=ProgressionService.session(h.getLevel().getServer()).inhabitants;identities.create(p.getUUID(),"forge"+p.getUUID().toString().substring(0,8),"",identities.available().getFirst());}
|
||||
catch(Exception e){throw new AssertionError(e);}
|
||||
bind(p,record);return p;
|
||||
}
|
||||
private static void bind(ServerPlayer p,ProgressRecord record){p.setAttached(ProgressionService.RECORD,record.json());ProgressEffects.apply(p,record);}
|
||||
}
|
||||
+1
@@ -68,6 +68,7 @@ public class HeadBlocks043GameTests {
|
||||
}
|
||||
@GameTest public void nativeWorkstationsOpenAndCloseAfterMovement(GameTestHelper h){
|
||||
var owner=player(h);var actor=player(h);actor.setPos(owner.position().add(1,0,0));
|
||||
for(var target:new String[]{"enchanting","alchemy","smithing"}){actor.setExperienceLevels(4);h.assertTrue(CraftAptitudes.buy(actor,target,0),"Native workstation fixture owns "+target);}
|
||||
Item[] items={Items.CRAFTING_TABLE,Items.CHEST,Items.TRAPPED_CHEST,Items.BARREL,Items.ENDER_CHEST,Items.SHULKER_BOX,
|
||||
Items.FURNACE,Items.BLAST_FURNACE,Items.SMOKER,Items.BREWING_STAND,Items.HOPPER,Items.DISPENSER,Items.DROPPER,
|
||||
Items.CRAFTER,Items.STONECUTTER,Items.SMITHING_TABLE,Items.LOOM,Items.CARTOGRAPHY_TABLE,Items.GRINDSTONE,Items.ANVIL,Items.ENCHANTING_TABLE,Items.BEACON};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"entrypoints": {
|
||||
"client": ["fr.koka.sanctuary.gametest.Starter039TestSupport"],
|
||||
"preLaunch": ["fr.koka.sanctuary.gametest.Inventory010MixinAudit"],
|
||||
"fabric-gametest": ["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"],
|
||||
"fabric-gametest": ["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"],
|
||||
"fabric-client-gametest": ["fr.koka.sanctuary.gametest.SanctuaryClientRenderTests"]
|
||||
},
|
||||
"mixins": ["sanctuary-gametest.mixins.json"],
|
||||
|
||||
@@ -44,7 +44,7 @@ public final class AtlasService {
|
||||
var session=ProgressionService.session(player.level().getServer());
|
||||
var record=ProgressEffects.get(player);
|
||||
return session!=null && session.healthy && player.level().dimension()==Level.OVERWORLD
|
||||
&& (operator ? canOperate(player) : record!=null && record.atlas());
|
||||
&& (operator ? canOperate(player) : record!=null && record.atlas() && fr.koka.sanctuary.progression.AptitudeService.enabled(player,"atlas"));
|
||||
}
|
||||
private static State state(ServerPlayer player) {
|
||||
var server=player.level().getServer();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.gui.layouts.LinearLayout;
|
||||
|
||||
public final class AptitudeClient {
|
||||
private static boolean ready;
|
||||
private static long pendingUntil;
|
||||
private AptitudeClient() {}
|
||||
public static boolean enabled(String target) {
|
||||
var p=Minecraft.getInstance().player;
|
||||
return p==null || AptitudeService.enabled(p,target);
|
||||
}
|
||||
public static void register() {
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(AptitudeService.Capability.TYPE,(p,c)->{});
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(AptitudeService.PermanentCapability.TYPE,(p,c)->{});
|
||||
ClientConfigurationNetworking.registerGlobalReceiver(AptitudeService.SmithingCapability.TYPE,(p,c)->{});
|
||||
ClientPlayNetworking.registerGlobalReceiver(AptitudeService.SmithingState.TYPE,(state,c)->c.client().execute(()->{
|
||||
var player=c.client().player;
|
||||
if(player==null || player.getId()!=state.entity())return;
|
||||
player.setAttached(CraftAptitudes.SMITHING,state.learned());
|
||||
if(c.client().gui.screen() instanceof ProgressionScreen screen)screen.accept("");
|
||||
}));
|
||||
ClientPlayNetworking.registerGlobalReceiver(AptitudeService.State.TYPE,(state,c)->c.client().execute(()->{
|
||||
var player=c.client().player;
|
||||
if(player==null || player.getId()!=state.entity())return;
|
||||
var value=new AptitudePreferences(state.disabledMask());
|
||||
boolean mapChanged=enabled("atlas")!=value.enabled("atlas");
|
||||
player.setAttached(AptitudeService.PREFERENCES,value.disabledMask());
|
||||
player.setAttached(CraftAptitudes.ENCHANTING,state.enchanting());
|
||||
player.setAttached(CraftAptitudes.ALCHEMY,state.alchemy());ready=true;pendingUntil=0;
|
||||
if(mapChanged && c.client().gui.screen() instanceof SanctuaryAtlasScreen map)map.permissionsChanged();
|
||||
if(c.client().gui.screen() instanceof ProgressionScreen screen)screen.accept("");
|
||||
}));
|
||||
ClientPlayConnectionEvents.DISCONNECT.register((h,c)->reset());
|
||||
ClientConfigurationConnectionEvents.INIT.register((h,c)->reset());
|
||||
}
|
||||
private static void reset(){ready=false;pendingUntil=0;}
|
||||
public static void acquired(LinearLayout group,int width) {
|
||||
var button=group.addChild(Button.builder(ProgressionService.label("aptitude_acquired"),b->{}).width(width)
|
||||
.tooltip(Tooltip.create(ProgressionService.label("aptitude_acquired_hint"))).build());
|
||||
button.active=false;
|
||||
}
|
||||
public static void toggle(LinearLayout group,int width,String target,boolean learned) {
|
||||
if(!AptitudePreferences.TOGGLEABLE.contains(target))throw new IllegalArgumentException("Permanent aptitude has no switch");
|
||||
var button=group.addChild(Button.builder(ProgressionService.label(enabled(target)?"aptitude_enabled":"aptitude_disabled"),b->{
|
||||
var player=Minecraft.getInstance().player;
|
||||
if(player==null || !ready || System.nanoTime()<pendingUntil || !ClientPlayNetworking.canSend(AptitudeService.Set.TYPE))return;
|
||||
pendingUntil=System.nanoTime()+4_000_000_000L;b.active=false;
|
||||
if(Minecraft.getInstance().gui.screen() instanceof ProgressionScreen screen)screen.waitForResponse();
|
||||
ClientPlayNetworking.send(new AptitudeService.Set(target,!enabled(target),AptitudeService.preferences(player).disabledMask()));
|
||||
}).width(width).tooltip(Tooltip.create(ProgressionService.label(enabled(target)?"aptitude_disable_hint":"aptitude_enable_hint")
|
||||
.copy().append("\n").append(ProgressionService.label("aptitude_toggle_hint")))).build());
|
||||
button.active=learned && ready && System.nanoTime()>=pendingUntil;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import fr.koka.sanctuary.atlas.AtlasPayloads;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
|
||||
/** No cross-server atlas cache: responses belong only to the currently open screen/token. */
|
||||
public final class AtlasClient {
|
||||
private static int sequence;
|
||||
private static KeyMapping mapKey;
|
||||
private AtlasClient() { }
|
||||
public static void register() {
|
||||
ClientPlayNetworking.registerGlobalReceiver(fr.koka.sanctuary.atlas.AtlasMarkerPayload.TYPE,(p,c) -> c.client().execute(() -> {
|
||||
@@ -28,49 +19,6 @@ public final class AtlasClient {
|
||||
else if (c.client().gui.screen() instanceof PauseAtlasAccess pause && pause.sanctuary$pauseMap()!=null) pause.sanctuary$pauseMap().accept(p);
|
||||
}));
|
||||
}
|
||||
public static void registerShortcut(KeyMapping.Category category) {
|
||||
var shortcut=new MapKey(category);
|
||||
mapKey=KeyMappingHelper.registerKeyMapping(shortcut);
|
||||
ClientLifecycleEvents.CLIENT_STARTED.register(client->shortcut.resolveLayout());
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client->{
|
||||
while(mapKey.consumeClick())if(client.gui.screen()==null)openFromShortcut(null);
|
||||
});
|
||||
}
|
||||
public static boolean openFromKey(Screen parent,KeyEvent event) {
|
||||
return mapKey!=null && mapKey.matches(event) && openFromShortcut(parent);
|
||||
}
|
||||
private static boolean openFromShortcut(Screen parent) {
|
||||
var client=Minecraft.getInstance();
|
||||
if(client.player==null || client.level==null || !client.player.isAlive() || !ProgressionClient.active())return false;
|
||||
if(!ProgressionClient.progress.atlas())client.player.sendOverlayMessage(SanctuaryAtlasScreen.label("locked"));
|
||||
else ProgressionClient.open(parent,"history");
|
||||
return true;
|
||||
}
|
||||
private static final class MapKey extends KeyMapping {
|
||||
private InputConstants.Key layoutDefault;
|
||||
private final boolean savedBinding;
|
||||
MapKey(Category category) {
|
||||
super("sanctuary.atlas.key",InputConstants.Type.KEYBOARD,InputConstants.KEY_M,category);
|
||||
layoutDefault=super.getDefaultKey();
|
||||
// An explicit saved binding, including Unbound, takes precedence over the layout default.
|
||||
var options=net.fabricmc.loader.api.FabricLoader.getInstance().getGameDir().resolve("options.txt");
|
||||
boolean saved=false;
|
||||
if(java.nio.file.Files.exists(options))try(var lines=java.nio.file.Files.lines(options)) {
|
||||
saved=lines.anyMatch(line->line.startsWith("key_"+getName()+":"));
|
||||
}catch(java.io.IOException error){saved=true;}
|
||||
savedBinding=saved;
|
||||
}
|
||||
void resolveLayout() {
|
||||
// Resolve the letter M after native startup: KEY_M is only its QWERTY position.
|
||||
boolean useDefault=!savedBinding && isDefault();
|
||||
int scan=org.lwjgl.sdl.SDLKeyboard.SDL_GetScancodeFromKey(InputConstants.KEYCODE_M,(java.nio.ShortBuffer)null);
|
||||
if(scan>0)layoutDefault=InputConstants.Type.KEYBOARD.getOrCreate(scan);
|
||||
if(useDefault)setKey(layoutDefault);
|
||||
KeyMapping.resetMapping();
|
||||
}
|
||||
@Override public InputConstants.Key getDefaultKey(){return layoutDefault;}
|
||||
@Override public boolean isDefault(){return key.equals(layoutDefault);}
|
||||
}
|
||||
public static int nextToken() {sequence=sequence==Integer.MAX_VALUE ? 1 : sequence+1;return sequence;}
|
||||
public static void send(AtlasPayloads.Request request) {
|
||||
if (ClientPlayNetworking.canSend(AtlasPayloads.Request.TYPE)) ClientPlayNetworking.send(request);
|
||||
|
||||
@@ -24,7 +24,7 @@ public final class CommunityPausePanels implements AutoCloseable {
|
||||
public CommunityPausePanels(Screen screen,Consumer<AbstractWidget> add,List<Button> nativeButtons) {
|
||||
this.screen=screen;
|
||||
boolean compact=screen.height<280;
|
||||
boolean unlocked=ProgressionClient.progress.atlas();
|
||||
boolean unlocked=ProgressionClient.progress.atlas() && AptitudeClient.enabled("atlas");
|
||||
int margin=8,gap=10,top=compact?46:50,bottom=screen.height-39;
|
||||
String[] sections={"blocks","identity","progress","faction","story","battle"};
|
||||
int columnGap=compact?4:8;
|
||||
|
||||
@@ -117,10 +117,11 @@ public final class CompanionClient {
|
||||
var buy=section.addChild(Button.builder(label("buy",config.activeCost()),b->action("buy",-1)).width(width).build());
|
||||
buy.active=config.enabled()&&Minecraft.getInstance().player.experienceLevel>=config.activeCost();
|
||||
}
|
||||
if(record!=null&&record.unlocked())section.addChild(new StringWidget(fr.koka.sanctuary.progression.ProgressionService.label("completed"),font));
|
||||
if(record!=null&&record.unlocked())AptitudeClient.acquired(section,width);
|
||||
var study=contents.addChild(LinearLayout.vertical().spacing(4));
|
||||
AptitudeHeading.add(study,width,font,"familiar",label("study"));
|
||||
study.addChild(new MultiLineTextWidget(label("study_description"),font).setMaxWidth(width));
|
||||
if(studied){AptitudeClient.acquired(study,width);return;}
|
||||
var buy=study.addChild(Button.builder(label(studyInvalid?"storage_invalid":studied?"study_learned":"study_buy",CompanionStudy.COST),b->action("buy_study",-1)).width(width).build());
|
||||
buy.active=!studied&&!studyInvalid&&record!=null&&config.enabled()&&Minecraft.getInstance().player.experienceLevel>=CompanionStudy.COST;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ 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;
|
||||
boolean details=CompanionClient.studied||ProgressionClient.operator;
|
||||
boolean details=(CompanionClient.studied||ProgressionClient.operator);
|
||||
var extra=new java.util.ArrayList<Component>();
|
||||
extra.add(rarity(stack).label().copy().withStyle(color(stack)));
|
||||
if(fr.koka.sanctuary.cosmetics.FamiliarEntity.burnsInDaylight(stack))
|
||||
|
||||
@@ -34,5 +34,6 @@ public final class FoodKnowledgeClient {
|
||||
boolean learned=ready()&&knowledge.studied();
|
||||
var button=group.addChild(Button.builder(label(invalid?"invalid":learned?"learned":"buy",FoodKnowledge.COST),b->{if(ClientPlayNetworking.canSend(FoodKnowledgeService.Buy.TYPE))ClientPlayNetworking.send(new FoodKnowledgeService.Buy());}).width(width).build());
|
||||
button.active=ready()&&!learned&&Minecraft.getInstance().player.experienceLevel>=FoodKnowledge.COST;
|
||||
if(learned)group.addChild(new MultiLineTextWidget(fr.koka.sanctuary.progression.ProgressionService.label("aptitude_permanent"),font).setMaxWidth(width));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public final class LyingClient {
|
||||
if(current==3)continue; // The server ends a dive on contact, not on another key press.
|
||||
int desired=m.options.keyShift.isDown() ? (current==2 ? 0 : 2) : (current!=0 ? 0 : 1);
|
||||
if(desired==1&&!m.player.onGround()&&!m.player.isInWater())desired=3;
|
||||
boolean learned=desired==0 || m.player.isCreative() || (desired==1 || desired==3 || ProgressionClient.progress.resting());
|
||||
boolean learned=(desired==0 || m.player.isCreative() || desired==1 || desired==3 || ProgressionClient.progress.resting());
|
||||
if(!learned)m.player.sendOverlayMessage(ProgressionService.label("resting_locked"));
|
||||
else ClientPlayNetworking.send(new LyingService.Request(desired,ProgressionClient.progress.revision()));
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public final class ProgressionClient {
|
||||
else if (active()) gui.setScreen(section.equals("history") ? new SanctuaryAtlasScreen(parent) : new ProgressionScreen(parent,section));
|
||||
}
|
||||
public static void register() {
|
||||
AptitudeClient.register();
|
||||
IntroClient.register();
|
||||
fr.koka.sanctuary.client.AccessoryClient.register();
|
||||
AtlasClient.register();
|
||||
|
||||
@@ -92,24 +92,47 @@ public final class ProgressionScreen extends BlocodexFrame {
|
||||
skills=scroll(skillContents,contentX+6,listTop,Math.max(24,listBottom-listTop),skillOffset);
|
||||
int innerWidth=contentWidth-panel-32;
|
||||
LinearLayout contents=LinearLayout.vertical().spacing(12);
|
||||
for(String target:ProgressRecord.APTITUDES) {
|
||||
var aptitude=contents.addChild(LinearLayout.vertical().spacing(4));
|
||||
String icon=switch(target){case "atlas"->"worldmap";case "swimming"->"swim";case "resting"->"rest";default->target;};
|
||||
AptitudeHeading.add(aptitude,innerWidth,font,icon,ProgressionService.label(target));
|
||||
var description=ProgressionService.label(target+"_description");
|
||||
if(target.equals("lying") || target.equals("resting"))description=description.copy().append("\n").append(LyingClient.controls(target.equals("resting")));
|
||||
if(target.equals("sorting"))description=description.copy().append("\n").append(InventorySortClient.controls());
|
||||
aptitude.addChild(new MultiLineTextWidget(description,font).setMaxWidth(innerWidth));
|
||||
aptitude.addChild(purchaseButton(target,innerWidth));
|
||||
for(var theme:List.of(
|
||||
List.of("survival","swimming","resting","food_knowledge"),
|
||||
List.of("inventory_crafting","sorting","catalogue"),
|
||||
List.of("familiars","active_bond","familiar_study"),
|
||||
List.of("exploration","atlas","names","zoom"),
|
||||
List.of("crafts","enchanting","alchemy","smithing"))) {
|
||||
contents.addChild(new MultiLineTextWidget(ProgressionService.label("theme_"+theme.getFirst()).copy()
|
||||
.withStyle(net.minecraft.ChatFormatting.AQUA),font).setMaxWidth(innerWidth));
|
||||
for(String target:theme.subList(1,theme.size())) {
|
||||
switch(target) {
|
||||
case "active_bond" -> CompanionClient.aptitude(contents,innerWidth,font,this);
|
||||
case "familiar_study" -> {} // Rendered alongside Active Bond by the familiar module.
|
||||
case "catalogue" -> RecipeClient.aptitude(contents,innerWidth,font);
|
||||
case "food_knowledge" -> FoodKnowledgeClient.aptitude(contents,innerWidth,font);
|
||||
case "zoom" -> ZoomClient.aptitude(contents,innerWidth,font);
|
||||
default -> aptitude(contents,innerWidth,target);
|
||||
}
|
||||
}
|
||||
}
|
||||
double previousScroll=aptitudes==null ? 0 : aptitudes.getScrollAmount();
|
||||
CompanionClient.aptitude(contents,innerWidth,font,this);
|
||||
RecipeClient.aptitude(contents,innerWidth,font);
|
||||
FoodKnowledgeClient.aptitude(contents,innerWidth,font);
|
||||
ZoomClient.aptitude(contents,innerWidth,font);
|
||||
aptitudes=scroll(contents,contentX+panel+16,listTop,listBottom-listTop,previousScroll);
|
||||
}
|
||||
|
||||
private void aptitude(LinearLayout contents,int width,String target) {
|
||||
var section=contents.addChild(LinearLayout.vertical().spacing(4));
|
||||
String icon=switch(target){case "atlas"->"worldmap";case "swimming"->"swim";case "resting"->"rest";default->target;};
|
||||
AptitudeHeading.add(section,width,font,icon,ProgressionService.label(target));
|
||||
var description=ProgressionService.label(target+"_description");
|
||||
if(target.equals("resting"))description=description.copy().append("\n").append(LyingClient.controls(true));
|
||||
if(target.equals("sorting"))description=description.copy().append("\n").append(InventorySortClient.controls());
|
||||
section.addChild(new MultiLineTextWidget(description,font).setMaxWidth(width));
|
||||
boolean learned=CraftAptitudes.target(target) ? CraftAptitudes.learned(minecraft.player,target) : ProgressionClient.progress.rank(target)==1;
|
||||
if(learned) {
|
||||
if(AptitudePreferences.TOGGLEABLE.contains(target))AptitudeClient.toggle(section,width,target,true);
|
||||
else AptitudeClient.acquired(section,width);
|
||||
}
|
||||
else section.addChild(purchaseButton(target,width));
|
||||
}
|
||||
|
||||
public void waitForResponse() {pendingTicks=80;message="";purchases.forEach(button->button.active=false);}
|
||||
|
||||
private StringWidget textCell(LinearLayout row,int width,Component text) {
|
||||
// StringWidget reports its text width in 26.3; the frame reserves a stable column.
|
||||
return row.addChild(new FrameLayout(width,20)).addChild(new StringWidget(width,20,text,font)
|
||||
@@ -194,13 +217,13 @@ public final class ProgressionScreen extends BlocodexFrame {
|
||||
}
|
||||
|
||||
private Button purchaseButton(String target,int width) {
|
||||
int cost=ProgressionClient.progress.cost(target,ProgressionClient.config);
|
||||
int cost=CraftAptitudes.target(target) ? CraftAptitudes.COST : ProgressionClient.progress.cost(target,ProgressionClient.config);
|
||||
Button button=Button.builder(cost<0 ? ProgressionService.label("completed")
|
||||
: ProgressionService.label(ProgressRecord.APTITUDES.contains(target) ? "unlock_cost" : "upgrade_cost",cost),b -> {
|
||||
: ProgressionService.label(ProgressRecord.APTITUDES.contains(target) || CraftAptitudes.target(target) ? "unlock_cost" : "upgrade_cost",cost),b -> {
|
||||
pendingTicks=80;message="";purchases.forEach(widget -> widget.active=false);
|
||||
ProgressionClient.action(target);
|
||||
}).width(width).build();
|
||||
button.active=ProgressionClient.progress.canRecord() && cost>=0 && minecraft.player!=null && minecraft.player.experienceLevel>=cost && pendingTicks==0;
|
||||
button.active=(CraftAptitudes.target(target) || ProgressionClient.progress.canRecord()) && cost>=0 && minecraft.player!=null && minecraft.player.experienceLevel>=cost && pendingTicks==0;
|
||||
purchases.add(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public final class RecipeClient {
|
||||
String key="sanctuary.recipes.collection."+id.replace(':','.');
|
||||
return Component.translatableWithFallback(key,id);
|
||||
}
|
||||
public static boolean allowed(){return ready&&!invalid&&(!active||operator||knowledge!=null&&knowledge.learned());}
|
||||
public static boolean allowed(){return ready&&!invalid&&(!active||(operator||knowledge!=null&&knowledge.learned()));}
|
||||
public static void reset(){ready=false;knowledge=null;active=false;invalid=false;operator=false;operatorAllowed=false;PENDING.setLength(0);revision=-1;nextPart=0;applied="";}
|
||||
public static void request(String action){if(ClientPlayNetworking.canSend(RecipePayloads.Request.TYPE))ClientPlayNetworking.send(new RecipePayloads.Request(action));}
|
||||
private static void accept(RecipePayloads.Snapshot p){
|
||||
@@ -73,6 +73,7 @@ public final class RecipeClient {
|
||||
var section=contents.addChild(LinearLayout.vertical().spacing(4));
|
||||
AptitudeHeading.add(section,width,font,"recipes",label("catalogue"));
|
||||
section.addChild(new MultiLineTextWidget(label("description"),font).setMaxWidth(width));
|
||||
if(knowledge!=null && knowledge.learned()){AptitudeClient.acquired(section,width);return;}
|
||||
var buy=section.addChild(Button.builder(label(invalid?"invalid":knowledge!=null&&knowledge.learned()?"learned":"buy",RecipeKnowledge.COST),b->request("buy")).width(width).build());
|
||||
buy.active=ready&&!invalid&&knowledge!=null&&!knowledge.learned()&&xp>=RecipeKnowledge.COST;
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ public final class SanctuaryBlocodexScreen extends DiscoveryFrame {
|
||||
}
|
||||
|
||||
private static boolean canBrowseAll() {
|
||||
return ProgressionClient.operator || RecipeClient.ready && !RecipeClient.invalid && RecipeClient.knowledge!=null && RecipeClient.knowledge.learned();
|
||||
return (ProgressionClient.operator || RecipeClient.ready && !RecipeClient.invalid && RecipeClient.knowledge!=null && RecipeClient.knowledge.learned());
|
||||
}
|
||||
private List<Filter> availableFilters() {
|
||||
return fullCatalogue ? List.of(Filter.POSSESSED,Filter.ALL,Filter.SEEN,Filter.MINED,Filter.DROPPED,Filter.PLACED,Filter.CARRIED)
|
||||
|
||||
@@ -53,7 +53,6 @@ public final class SanctuaryKnowledgeClient implements ClientModInitializer {
|
||||
GraveInventoryScreen.register();
|
||||
TutorialClient.register();
|
||||
var category=KeyMapping.Category.register(SanctuaryMod.id("sanctuary"));
|
||||
AtlasClient.registerShortcut(category);
|
||||
DynamicLightClient.register(category);
|
||||
ZoomClient.register(category);
|
||||
LyingClient.register(category);
|
||||
|
||||
@@ -84,7 +84,7 @@ public final class SanctuaryMobNames {
|
||||
}
|
||||
|
||||
public static boolean shouldDisplay(Entity entity) {
|
||||
return ProgressionClient.active() && ProgressionClient.progress.names() && ProgressionClient.names.enabled() && isTargeted(entity)
|
||||
return ProgressionClient.active() && ProgressionClient.progress.names() && AptitudeClient.enabled("names") && ProgressionClient.names.enabled() && isTargeted(entity)
|
||||
&& entity instanceof Mob && !entity.hasCustomName();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ public final class ZoomClient {
|
||||
var group=contents.addChild(LinearLayout.vertical().spacing(4));
|
||||
AptitudeHeading.add(group,width,font,"zoom",label("title"));
|
||||
group.addChild(new MultiLineTextWidget(label("description",key.getTranslatedKeyMessage()),font).setMaxWidth(width));
|
||||
if(learned()){AptitudeClient.acquired(group,width);return;}
|
||||
var button=group.addChild(Button.builder(label(learned()?"learned":"buy",ZoomService.COST),b->{
|
||||
if(ClientPlayNetworking.canSend(ZoomService.Buy.TYPE))ClientPlayNetworking.send(new ZoomService.Buy());
|
||||
}).width(width).build());
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.progression.CraftAptitudes;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/** Anvil overrides the combiner pickup check; Smithing inherits it. */
|
||||
@Mixin({ItemCombinerMenu.class,AnvilMenu.class})
|
||||
abstract class CraftAptitudeCombinerMixin {
|
||||
@Inject(method="mayPickup",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$forgePickup(Player player,boolean hasItem,CallbackInfoReturnable<Boolean> ci) {
|
||||
if(!CraftAptitudes.allowed(player,(AbstractContainerMenu)(Object)this))ci.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.progression.CraftAptitudes;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.EnchantmentMenu;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(EnchantmentMenu.class)
|
||||
abstract class CraftAptitudeEnchantMixin {
|
||||
@Inject(method="clickMenuButton",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$enchantAccess(Player player,int button,CallbackInfoReturnable<Boolean> ci) {
|
||||
if(!CraftAptitudes.allowed(player,"enchanting"))ci.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import fr.koka.sanctuary.progression.CraftAptitudes;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.*;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.*;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin({EnchantmentMenu.class,BrewingStandMenu.class,ItemCombinerMenu.class})
|
||||
abstract class CraftAptitudeMenuMixin {
|
||||
@Inject(method="stillValid",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$craftAccess(Player player,CallbackInfoReturnable<Boolean> ci) {
|
||||
if(!CraftAptitudes.allowed(player,(AbstractContainerMenu)(Object)this))ci.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.koka.sanctuary.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.*;
|
||||
import fr.koka.sanctuary.progression.*;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.MenuProvider;
|
||||
import net.minecraft.world.entity.player.*;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
@Mixin(ServerPlayer.class)
|
||||
abstract class CraftAptitudeOpenMixin {
|
||||
@WrapOperation(method="openMenu",at=@At(value="INVOKE",target="Lnet/minecraft/world/MenuProvider;createMenu(ILnet/minecraft/world/entity/player/Inventory;Lnet/minecraft/world/entity/player/Player;)Lnet/minecraft/world/inventory/AbstractContainerMenu;"))
|
||||
private AbstractContainerMenu sanctuary$craftAccess(MenuProvider provider,int id,Inventory inventory,Player player,Operation<AbstractContainerMenu> original) {
|
||||
var menu=original.call(provider,id,inventory,player);
|
||||
if(menu!=null && !CraftAptitudes.allowed(player,menu)) {
|
||||
((ServerPlayer)player).sendOverlayMessage(ProgressionService.label(CraftAptitudes.target(menu)+"_locked"));
|
||||
menu.removed(player);return null;
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.ProgressionClient;
|
||||
import fr.koka.sanctuary.client.AtlasClient;
|
||||
import fr.koka.sanctuary.client.PauseAtlasAccess;
|
||||
import fr.koka.sanctuary.client.PauseAtlasWidget;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
@@ -9,7 +8,6 @@ import fr.koka.sanctuary.client.CommunityPausePanels;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.PauseScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
@@ -32,10 +30,6 @@ public abstract class CommunityPauseMixin extends Screen implements PauseAtlasAc
|
||||
}
|
||||
@Inject(method="tick",at=@At("TAIL"))
|
||||
private void sanctuary$communityTick(CallbackInfo callback) { if(sanctuary$community!=null)sanctuary$community.tick(); }
|
||||
@Override public boolean keyPressed(KeyEvent event) {
|
||||
if(sanctuary$community!=null && AtlasClient.openFromKey(this,event))return true;
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
@Override public PauseAtlasWidget sanctuary$pauseMap() {return sanctuary$community==null?null:sanctuary$community.map();}
|
||||
@Inject(method="removed",at=@At("HEAD"))
|
||||
private void sanctuary$communityRemoved(CallbackInfo callback) {if(sanctuary$community!=null)sanctuary$community.close();}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Keep the beta.236 codec and bit order; only the first two choices remain effective. */
|
||||
public record AptitudePreferences(int disabledMask) {
|
||||
public static final List<String> TOGGLEABLE=List.of("names","atlas");
|
||||
public static final List<String> PERMANENT=List.of("sorting","swimming","resting",
|
||||
"active_bond","familiar_study","catalogue","zoom","enchanting","alchemy","smithing","food_knowledge");
|
||||
public static final int MAX_MASK=2047;
|
||||
public static final int ACTIVE_MASK=3;
|
||||
public static final AptitudePreferences DEFAULT=new AptitudePreferences(0);
|
||||
public AptitudePreferences {
|
||||
if(disabledMask<0 || disabledMask>MAX_MASK)throw new IllegalArgumentException("Invalid aptitude preferences");
|
||||
disabledMask&=ACTIVE_MASK;
|
||||
}
|
||||
private static int bit(String target) {
|
||||
int index=TOGGLEABLE.indexOf(target);
|
||||
if(index<0)throw new IllegalArgumentException("Unknown toggleable aptitude");
|
||||
return 1<<index;
|
||||
}
|
||||
public boolean enabled(String target) {return PERMANENT.contains(target) || (disabledMask&bit(target))==0;}
|
||||
public AptitudePreferences with(String target,boolean enabled) {
|
||||
int bit=bit(target);
|
||||
return new AptitudePreferences(enabled ? disabledMask&~bit : disabledMask|bit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.fabricmc.fabric.api.attachment.v1.*;
|
||||
import net.fabricmc.fabric.api.networking.v1.*;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
/** Personal, server-authoritative choices; never removes or rewrites a purchase. */
|
||||
public final class AptitudeService {
|
||||
public static final AttachmentType<Integer> PREFERENCES=AttachmentRegistry.create(SanctuaryMod.id("aptitude_preferences"),
|
||||
b->b.persistent(Codec.intRange(0,AptitudePreferences.MAX_MASK)).copyOnDeath());
|
||||
private AptitudeService() {}
|
||||
public record Capability() implements CustomPacketPayload {
|
||||
public static final Type<Capability> TYPE=new Type<>(SanctuaryMod.id("aptitude_preferences_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf,Capability> CODEC=StreamCodec.unit(new Capability());
|
||||
public Type<Capability> type(){return TYPE;}
|
||||
}
|
||||
public record PermanentCapability() implements CustomPacketPayload {
|
||||
public static final Type<PermanentCapability> TYPE=new Type<>(SanctuaryMod.id("aptitude_permanent_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf,PermanentCapability> CODEC=StreamCodec.unit(new PermanentCapability());
|
||||
public Type<PermanentCapability> type(){return TYPE;}
|
||||
}
|
||||
public record SmithingCapability() implements CustomPacketPayload {
|
||||
public static final Type<SmithingCapability> TYPE=new Type<>(SanctuaryMod.id("smithing_v1"));
|
||||
public static final StreamCodec<FriendlyByteBuf,SmithingCapability> CODEC=StreamCodec.unit(new SmithingCapability());
|
||||
public Type<SmithingCapability> type(){return TYPE;}
|
||||
}
|
||||
public record SmithingState(int entity,boolean learned) implements CustomPacketPayload {
|
||||
public static final Type<SmithingState> TYPE=new Type<>(SanctuaryMod.id("smithing_state_v1"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,SmithingState> CODEC=CustomPacketPayload.codec(
|
||||
(p,b)->{b.writeVarInt(p.entity);b.writeBoolean(p.learned);},b->new SmithingState(b.readVarInt(),b.readBoolean()));
|
||||
public Type<SmithingState> type(){return TYPE;}
|
||||
}
|
||||
public record Set(String target,boolean enabled,int expectedMask) implements CustomPacketPayload {
|
||||
public static final Type<Set> TYPE=new Type<>(SanctuaryMod.id("aptitude_set_v1"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,Set> CODEC=CustomPacketPayload.codec(
|
||||
(p,b)->{b.writeUtf(p.target,32);b.writeBoolean(p.enabled);b.writeVarInt(p.expectedMask);},
|
||||
b->new Set(b.readUtf(32),b.readBoolean(),b.readVarInt()));
|
||||
public Type<Set> type(){return TYPE;}
|
||||
}
|
||||
public record State(int entity,int disabledMask,boolean enchanting,boolean alchemy) implements CustomPacketPayload {
|
||||
public static final Type<State> TYPE=new Type<>(SanctuaryMod.id("aptitude_state_v1"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf,State> CODEC=CustomPacketPayload.codec(
|
||||
(p,b)->{b.writeVarInt(p.entity);b.writeVarInt(p.disabledMask);b.writeBoolean(p.enchanting);b.writeBoolean(p.alchemy);},
|
||||
b->new State(b.readVarInt(),b.readVarInt(),b.readBoolean(),b.readBoolean()));
|
||||
public Type<State> type(){return TYPE;}
|
||||
}
|
||||
public static AptitudePreferences preferences(Player p) {return new AptitudePreferences(p.getAttachedOrElse(PREFERENCES,0));}
|
||||
public static boolean enabled(Player p,String target) {return preferences(p).enabled(target);}
|
||||
public static boolean owned(ServerPlayer p,String target) {
|
||||
var progress=ProgressEffects.get(p);
|
||||
if(progress==null)return false;
|
||||
return AptitudePreferences.TOGGLEABLE.contains(target) && progress.rank(target)==1;
|
||||
}
|
||||
public static boolean set(ServerPlayer p,Set request) {
|
||||
var session=ProgressionService.session(p.level().getServer());
|
||||
if(session==null || !session.healthy || !p.isAlive() || p.isSpectator()
|
||||
|| !AptitudePreferences.TOGGLEABLE.contains(request.target()))return false;
|
||||
var old=preferences(p);
|
||||
if(request.expectedMask()!=old.disabledMask())return false;
|
||||
try {if(!owned(p,request.target()))return false;}
|
||||
catch(RuntimeException invalid){return false;}
|
||||
var next=old.with(request.target(),request.enabled());
|
||||
if(!next.equals(old))p.setAttached(PREFERENCES,next.disabledMask());
|
||||
return true;
|
||||
}
|
||||
public static void send(ServerPlayer p) {
|
||||
// Normalize legacy preferences on sync without altering purchases or progression.
|
||||
int mask=preferences(p).disabledMask();
|
||||
if(p.getAttachedOrElse(PREFERENCES,0)!=mask)p.setAttached(PREFERENCES,mask);
|
||||
if(ServerPlayNetworking.canSend(p,State.TYPE))ServerPlayNetworking.send(p,new State(p.getId(),preferences(p).disabledMask(),CraftAptitudes.learned(p,"enchanting"),CraftAptitudes.learned(p,"alchemy")));
|
||||
if(ServerPlayNetworking.canSend(p,SmithingState.TYPE))ServerPlayNetworking.send(p,new SmithingState(p.getId(),CraftAptitudes.learned(p,"smithing")));
|
||||
}
|
||||
public static void register() {
|
||||
java.util.Objects.requireNonNull(CraftAptitudes.ENCHANTING);
|
||||
java.util.Objects.requireNonNull(CraftAptitudes.ALCHEMY);
|
||||
java.util.Objects.requireNonNull(CraftAptitudes.SMITHING);
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(Capability.TYPE,Capability.CODEC);
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(PermanentCapability.TYPE,PermanentCapability.CODEC);
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(SmithingCapability.TYPE,SmithingCapability.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(SmithingState.TYPE,SmithingState.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(Set.TYPE,Set.CODEC);
|
||||
PayloadTypeRegistry.clientboundPlay().register(State.TYPE,State.CODEC);
|
||||
ServerPlayNetworking.registerGlobalReceiver(Set.TYPE,(request,c)->c.server().execute(()->{
|
||||
boolean accepted=set(c.player(),request);
|
||||
ProgressionService.send(c.player(),accepted ? "" : "aptitude_refused");
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fr.koka.sanctuary.progression;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import fr.koka.sanctuary.SanctuaryMod;
|
||||
import net.fabricmc.fabric.api.attachment.v1.*;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.*;
|
||||
|
||||
/** Access to personal workstations; shared brewing processes keep vanilla behavior. */
|
||||
public final class CraftAptitudes {
|
||||
public static final int COST=4;
|
||||
public static final AttachmentType<Boolean> ENCHANTING=AttachmentRegistry.create(SanctuaryMod.id("enchanting_learned"),
|
||||
b->b.persistent(Codec.BOOL).copyOnDeath());
|
||||
public static final AttachmentType<Boolean> ALCHEMY=AttachmentRegistry.create(SanctuaryMod.id("alchemy_learned"),
|
||||
b->b.persistent(Codec.BOOL).copyOnDeath());
|
||||
public static final AttachmentType<Boolean> SMITHING=AttachmentRegistry.create(SanctuaryMod.id("smithing_learned"),
|
||||
b->b.persistent(Codec.BOOL).copyOnDeath());
|
||||
private CraftAptitudes() {}
|
||||
public static boolean target(String target){return target.equals("enchanting") || target.equals("alchemy") || target.equals("smithing");}
|
||||
private static AttachmentType<Boolean> attachment(String target) {
|
||||
return switch(target){case "enchanting"->ENCHANTING;case "alchemy"->ALCHEMY;case "smithing"->SMITHING;default->throw new IllegalArgumentException("Unknown craft aptitude");};
|
||||
}
|
||||
public static boolean learned(Player p,String target){return p.getAttachedOrElse(attachment(target),false);}
|
||||
public static String target(AbstractContainerMenu menu){return menu instanceof EnchantmentMenu ? "enchanting" : menu instanceof BrewingStandMenu ? "alchemy" : menu instanceof AnvilMenu || menu instanceof SmithingMenu ? "smithing" : "";}
|
||||
public static boolean allowed(Player p,String target) {
|
||||
if(ProgressEffects.get(p)==null)return true;
|
||||
if(p instanceof ServerPlayer server) {
|
||||
var session=ProgressionService.session(server.level().getServer());
|
||||
if(session==null || !session.healthy)return false;
|
||||
}
|
||||
return learned(p,target);
|
||||
}
|
||||
public static boolean allowed(Player p,AbstractContainerMenu menu) {
|
||||
String target=target(menu);return target.isEmpty() || allowed(p,target);
|
||||
}
|
||||
public static boolean buy(ServerPlayer p,String target,int revision) {
|
||||
var session=ProgressionService.session(p.level().getServer());var progress=ProgressEffects.get(p);
|
||||
if(!target(target) || session==null || !session.healthy || progress==null || progress.revision()!=revision
|
||||
|| !p.isAlive() || p.isSpectator() || learned(p,target) || p.experienceLevel<COST)return false;
|
||||
p.giveExperienceLevels(-COST);p.setAttached(attachment(target),true);return true;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ public final class ProgressEffects {
|
||||
}
|
||||
public static boolean canSwim(Player player) {
|
||||
var progress=get(player);
|
||||
return progress==null || progress.swimming() || player.isCreative() || player.isSpectator();
|
||||
return progress==null || (progress.swimming() || player.isCreative() || player.isSpectator());
|
||||
}
|
||||
public static void bind(Player player, ProgressRecord progress) {
|
||||
PLAYERS.put(player, progress); FOOD.put(player.getFoodData(), progress);
|
||||
|
||||
@@ -67,6 +67,7 @@ public final class ProgressionService {
|
||||
PayloadTypeRegistry.clientboundConfiguration().register(AccessPayloads.Status.TYPE,AccessPayloads.Status.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(AccessPayloads.Check.TYPE,AccessPayloads.Check.CODEC);
|
||||
PayloadTypeRegistry.serverboundConfiguration().register(AccessPayloads.Admit.TYPE,AccessPayloads.Admit.CODEC);
|
||||
AptitudeService.register();
|
||||
SanctuarySitting.register();
|
||||
fr.koka.sanctuary.inventory.HotbarService.register();
|
||||
fr.koka.sanctuary.inventory.InventorySortService.register();
|
||||
@@ -116,6 +117,13 @@ public final class ProgressionService {
|
||||
if (!ServerConfigurationNetworking.canSend(handler,TutorialPayloads.Capability.TYPE)) {
|
||||
handler.disconnect(TutorialService.label("version_required"));return;
|
||||
}
|
||||
if (!ServerConfigurationNetworking.canSend(handler,AptitudeService.Capability.TYPE)
|
||||
|| !ServerConfigurationNetworking.canSend(handler,AptitudeService.PermanentCapability.TYPE)) {
|
||||
handler.disconnect(label("aptitude_version_required"));return;
|
||||
}
|
||||
if (!ServerConfigurationNetworking.canSend(handler,AptitudeService.SmithingCapability.TYPE)) {
|
||||
handler.disconnect(label("smithing_version_required"));return;
|
||||
}
|
||||
if (!ServerConfigurationNetworking.canSend(handler,ProgressionPayloads.SwimmingCapability.TYPE)) {
|
||||
handler.disconnect(label("swimming_version_required"));return;
|
||||
}
|
||||
@@ -522,6 +530,10 @@ public final class ProgressionService {
|
||||
boolean removed=refund(player,action.target().substring(7),action.revision());
|
||||
send(player,removed ? "refunded" : "refund_refused");return;
|
||||
}
|
||||
if (CraftAptitudes.target(action.target())) {
|
||||
boolean bought=CraftAptitudes.buy(player,action.target(),action.revision());
|
||||
send(player,bought ? "purchased" : "purchase_refused");return;
|
||||
}
|
||||
if (!ProgressEffects.get(player).canRecord()) {send(player,"history_full");return;}
|
||||
boolean bought=purchase(player,action.target(),action.revision());
|
||||
send(player,bought ? "purchased" : "purchase_refused");
|
||||
@@ -536,6 +548,7 @@ public final class ProgressionService {
|
||||
// The solo pause menu processes networking without ticking the player's XP synchronizer.
|
||||
player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetExperiencePacket(
|
||||
player.experienceProgress,player.totalExperience,player.experienceLevel));
|
||||
AptitudeService.send(player);
|
||||
ServerPlayNetworking.send(player,new ProgressionPayloads.Snapshot(
|
||||
JSON.toJson(session.inhabitants.get(player.getUUID())),player.getAttachedOrThrow(RECORD),JSON.toJson(session.config),message,canOperate(player)));
|
||||
player.inventoryMenu.broadcastFullState();
|
||||
|
||||
@@ -569,7 +569,7 @@
|
||||
"sanctuary.progression.sorting_version_required": "Inventory sorting requires matching Sanctuary beta.014 or later versions on the client and server.",
|
||||
"screen.sanctuary.inventory.sort_key": "Sort hovered inventory area",
|
||||
"screen.sanctuary.inventory.sort_hint": "%s: sort hovered storage; over the hotbar, sort only the hotbar.",
|
||||
"screen.sanctuary.inventory.sort_locked": "Unlock Inventory sorting in the Blocodex Aptitudes panel.",
|
||||
"screen.sanctuary.inventory.sort_locked": "Unlock or enable Inventory sorting in Progression.",
|
||||
"screen.sanctuary.inventory.sort_refused": "Sorting refused: check the menu and empty your cursor, then try again.",
|
||||
"sanctuary.tutorial.movement": "Move and look around",
|
||||
"sanctuary.tutorial.find_tree": "Find a tree",
|
||||
@@ -589,7 +589,7 @@
|
||||
"sanctuary.progression.resting": "Rest",
|
||||
"sanctuary.progression.resting_description": "Rest in the sleeping pose to regain half a heart every 10 s, using half a food icon. Moving or taking damage interrupts rest.",
|
||||
"sanctuary.progression.resting_controls": "Shift + %s to rest (remappable key)",
|
||||
"sanctuary.progression.resting_locked": "Unlock Rest in the Blocodex aptitudes.",
|
||||
"sanctuary.progression.resting_locked": "Unlock or enable Rest in Progression.",
|
||||
"sanctuary.progression.next_cost_short": "%s lv.",
|
||||
"sanctuary.progression.rank_max_short": "Max.",
|
||||
"sanctuary.progression.skill_bar_hint": "The bar shows acquired tiers; the number is the next tier’s cost in XP levels.",
|
||||
@@ -802,7 +802,7 @@
|
||||
"sanctuary.companion.unavailable": "Familiar powers are unavailable.",
|
||||
"sanctuary.companion.storage_invalid": "The bond data cannot be read. It has been preserved.",
|
||||
"sanctuary.companion.refused": "This action is not allowed here.",
|
||||
"sanctuary.companion.locked": "Unlock Active bond first.",
|
||||
"sanctuary.companion.locked": "Unlock or enable Active bond in Progression.",
|
||||
"sanctuary.companion.no_familiar": "Equip a familiar egg.",
|
||||
"sanctuary.companion.attuning": "This familiar is still attuning.",
|
||||
"sanctuary.companion.cooldown": "The power is still cooling down.",
|
||||
@@ -2452,7 +2452,6 @@
|
||||
"sanctuary.community.actions_for": "Actions · %s",
|
||||
"sanctuary.community.reply_hint": "Write a reply…",
|
||||
"sanctuary.pause.map": "World map",
|
||||
"sanctuary.atlas.key": "Open world map",
|
||||
"sanctuary.pause.map_hint": "Open the map and explore",
|
||||
"sanctuary.community.more": "Load more",
|
||||
"sanctuary.community.browse": "All publications",
|
||||
@@ -2619,5 +2618,30 @@
|
||||
"biome.sanctuary.diagonal_swamp229": "Upland Swamp",
|
||||
"biome.sanctuary.diagonal_pale229": "Sheltered Pale Garden",
|
||||
"biome.sanctuary.diagonal_sulfur229": "Sulfur Cavities",
|
||||
"biome.sanctuary.diagonal_pale230": "Lower Pale Garden"
|
||||
"biome.sanctuary.diagonal_pale230": "Lower Pale Garden",
|
||||
"sanctuary.progression.aptitude_enabled": "Enabled",
|
||||
"sanctuary.progression.aptitude_disabled": "Disabled",
|
||||
"sanctuary.progression.aptitude_toggle_hint": "Changing this choice is free and keeps your purchase.",
|
||||
"sanctuary.progression.aptitude_permanent": "Permanent knowledge",
|
||||
"sanctuary.progression.aptitude_refused": "The choice was not applied. Try again.",
|
||||
"sanctuary.progression.aptitude_version_required": "Permanent aptitudes require Sanctuary beta.237 or later on both client and server.",
|
||||
"sanctuary.progression.theme_survival": "Survival",
|
||||
"sanctuary.progression.theme_inventory_crafting": "Inventory and crafting",
|
||||
"sanctuary.progression.theme_familiars": "Familiars",
|
||||
"sanctuary.progression.theme_exploration": "Exploration",
|
||||
"sanctuary.progression.theme_crafts": "Arts and crafts",
|
||||
"sanctuary.progression.enchanting": "Enchanting",
|
||||
"sanctuary.progression.enchanting_description": "Use an enchanting table to enchant items.",
|
||||
"sanctuary.progression.enchanting_locked": "Unlock and enable Enchanting in Progression to use this table.",
|
||||
"sanctuary.progression.alchemy": "Alchemy",
|
||||
"sanctuary.progression.alchemy_description": "Use a brewing stand to brew potions.",
|
||||
"sanctuary.progression.alchemy_locked": "Unlock and enable Alchemy in Progression to use this brewing stand.",
|
||||
"sanctuary.progression.aptitude_disable_hint": "Click to disable.",
|
||||
"sanctuary.progression.aptitude_enable_hint": "Click to enable.",
|
||||
"sanctuary.progression.aptitude_acquired": "Acquired",
|
||||
"sanctuary.progression.aptitude_acquired_hint": "This aptitude remains permanently available after purchase.",
|
||||
"sanctuary.progression.smithing": "Smithing",
|
||||
"sanctuary.progression.smithing_description": "Use anvils and the smithing table to repair, rename and upgrade equipment.",
|
||||
"sanctuary.progression.smithing_locked": "Acquire Smithing in Progression to use an anvil or smithing table.",
|
||||
"sanctuary.progression.smithing_version_required": "Smithing requires Sanctuary beta.238 or later on both client and server."
|
||||
}
|
||||
|
||||
@@ -569,7 +569,7 @@
|
||||
"sanctuary.progression.sorting_version_required": "Le rangement nécessite la même version de Sanctuary beta.014 ou ultérieure sur le client et le serveur.",
|
||||
"screen.sanctuary.inventory.sort_key": "Ranger la zone d’inventaire visée",
|
||||
"screen.sanctuary.inventory.sort_hint": "%s : ranger le stockage visé ; sur la hotbar, ranger seulement la hotbar.",
|
||||
"screen.sanctuary.inventory.sort_locked": "Débloque Rangement dans les Aptitudes du Blocodex.",
|
||||
"screen.sanctuary.inventory.sort_locked": "Débloquez ou activez le tri d’inventaire dans Progression.",
|
||||
"screen.sanctuary.inventory.sort_refused": "Rangement refusé : vérifie le menu et libère le curseur, puis réessaie.",
|
||||
"sanctuary.tutorial.movement": "Se déplacer et regarder autour de soi",
|
||||
"sanctuary.tutorial.find_tree": "Trouver un arbre",
|
||||
@@ -589,7 +589,7 @@
|
||||
"sanctuary.progression.resting": "Se reposer",
|
||||
"sanctuary.progression.resting_description": "Repose-toi dans la posture de sommeil pour récupérer un demi-cœur toutes les 10 s, contre une demi-barre de faim. Bouger ou subir des dégâts interrompt le repos.",
|
||||
"sanctuary.progression.resting_controls": "Maj + %s pour se reposer (touche configurable)",
|
||||
"sanctuary.progression.resting_locked": "Débloque Se reposer dans les aptitudes du Blocodex.",
|
||||
"sanctuary.progression.resting_locked": "Débloquez ou activez Se reposer dans Progression.",
|
||||
"sanctuary.progression.next_cost_short": "%s niv.",
|
||||
"sanctuary.progression.rank_max_short": "Max.",
|
||||
"sanctuary.progression.skill_bar_hint": "La barre indique les paliers acquis ; le nombre est le prix du prochain palier en niveaux d’XP.",
|
||||
@@ -802,7 +802,7 @@
|
||||
"sanctuary.companion.unavailable": "Les pouvoirs de familiers sont indisponibles.",
|
||||
"sanctuary.companion.storage_invalid": "Les données du lien ne peuvent pas être lues. Elles ont été conservées.",
|
||||
"sanctuary.companion.refused": "Ce geste n’est pas autorisé ici.",
|
||||
"sanctuary.companion.locked": "Débloque d’abord l’aptitude Lien actif.",
|
||||
"sanctuary.companion.locked": "Débloquez ou activez Lien actif dans Progression.",
|
||||
"sanctuary.companion.no_familiar": "Équipe un œuf de familier.",
|
||||
"sanctuary.companion.attuning": "Le lien avec ce familier se forme encore.",
|
||||
"sanctuary.companion.cooldown": "Le pouvoir est encore en recharge.",
|
||||
@@ -2452,7 +2452,6 @@
|
||||
"sanctuary.community.actions_for": "Actions · %s",
|
||||
"sanctuary.community.reply_hint": "Écrire une réponse…",
|
||||
"sanctuary.pause.map": "Carte du monde",
|
||||
"sanctuary.atlas.key": "Ouvrir la carte",
|
||||
"sanctuary.pause.map_hint": "Ouvrir la carte et explorer",
|
||||
"sanctuary.community.more": "Charger la suite",
|
||||
"sanctuary.community.browse": "Toutes les publications",
|
||||
@@ -2619,5 +2618,30 @@
|
||||
"biome.sanctuary.diagonal_swamp229": "Marais perché",
|
||||
"biome.sanctuary.diagonal_pale229": "Jardin pâle abrité",
|
||||
"biome.sanctuary.diagonal_sulfur229": "Cavités de soufre",
|
||||
"biome.sanctuary.diagonal_pale230": "Jardin pâle inférieur"
|
||||
"biome.sanctuary.diagonal_pale230": "Jardin pâle inférieur",
|
||||
"sanctuary.progression.aptitude_enabled": "Activée",
|
||||
"sanctuary.progression.aptitude_disabled": "Désactivée",
|
||||
"sanctuary.progression.aptitude_toggle_hint": "Changer ce choix est gratuit et conserve votre achat.",
|
||||
"sanctuary.progression.aptitude_permanent": "Connaissance permanente",
|
||||
"sanctuary.progression.aptitude_refused": "Le choix n’a pas été appliqué. Réessayez.",
|
||||
"sanctuary.progression.aptitude_version_required": "Les aptitudes permanentes nécessitent Sanctuary beta.237 ou plus récent côté client et serveur.",
|
||||
"sanctuary.progression.theme_survival": "Survie",
|
||||
"sanctuary.progression.theme_inventory_crafting": "Inventaire et fabrication",
|
||||
"sanctuary.progression.theme_familiars": "Familiers",
|
||||
"sanctuary.progression.theme_exploration": "Exploration",
|
||||
"sanctuary.progression.theme_crafts": "Arts et métiers",
|
||||
"sanctuary.progression.enchanting": "Enchantement",
|
||||
"sanctuary.progression.enchanting_description": "Utiliser une table d’enchantement pour enchanter des objets.",
|
||||
"sanctuary.progression.enchanting_locked": "Débloquez et activez Enchantement dans Progression pour utiliser cette table.",
|
||||
"sanctuary.progression.alchemy": "Alchimie",
|
||||
"sanctuary.progression.alchemy_description": "Utiliser un alambic pour préparer des potions.",
|
||||
"sanctuary.progression.alchemy_locked": "Débloquez et activez Alchimie dans Progression pour utiliser cet alambic.",
|
||||
"sanctuary.progression.aptitude_disable_hint": "Cliquer pour désactiver.",
|
||||
"sanctuary.progression.aptitude_enable_hint": "Cliquer pour activer.",
|
||||
"sanctuary.progression.aptitude_acquired": "Acquise",
|
||||
"sanctuary.progression.aptitude_acquired_hint": "Cette aptitude est acquise définitivement après achat.",
|
||||
"sanctuary.progression.smithing": "Forgerie",
|
||||
"sanctuary.progression.smithing_description": "Utiliser les enclumes et la table de forge pour réparer, renommer et améliorer les équipements.",
|
||||
"sanctuary.progression.smithing_locked": "Acquérez Forgerie dans Progression pour utiliser une enclume ou une table de forge.",
|
||||
"sanctuary.progression.smithing_version_required": "Forgerie nécessite Sanctuary beta.238 ou plus récent côté client et serveur."
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 182 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 225 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 162 B |
@@ -3,6 +3,10 @@
|
||||
"package": "fr.koka.sanctuary.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": [
|
||||
"CraftAptitudeOpenMixin",
|
||||
"CraftAptitudeMenuMixin",
|
||||
"CraftAptitudeEnchantMixin",
|
||||
"CraftAptitudeCombinerMixin",
|
||||
"IslandChestLootMixin",
|
||||
"BadlandsBands211Accessor",
|
||||
"WestMonument217Accessor",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.236"
|
||||
version = "beta.238"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user