Compare commits

..
Author SHA1 Message Date
koka 5dee98106e Keep purchased aptitudes permanent and integrate Pause in beta.237
Build Sanctuary / build (push) Waiting to run
2026-10-06 14:43:01 +02:00
koka d6f3258442 Merge verified beta.235 Pause layout into progression aptitudes 2026-10-06 14:35:34 +02:00
koka 1c8e6437d5 Group aptitudes and unlock enchanting and alchemy in beta.236 2026-10-06 14:25:15 +02:00
36 changed files with 875 additions and 38 deletions
+13
View File
@@ -6,6 +6,19 @@ Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
correspondantes. Le module Sanctuary Test n’est pas requis.
[Intégration et vérification du pack beta.224](docs/four-islands-base224.md).
## beta.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
[Disposition adaptative](docs/pause-columns-beta235.md) : avant Atlas, Aventure
+140
View File
@@ -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.
+99
View File
@@ -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.
+9
View File
@@ -1,5 +1,14 @@
# Backlog Sanctuary
## 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
Branche `codex/pause-columns-beta235`, base beta.234. Avant Atlas : Aventure à
+2 -2
View File
@@ -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.235
pack_version=beta.235
mod_version=beta.237
pack_version=beta.237
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3
+4 -1
View File
@@ -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 = [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') }
@@ -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"));
@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("Aptitudes237 "+UUID.randomUUID());s.setSeed("237");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"),"New workstations start locked");});
for(String craft:List.of("enchanting","alchemy")) {
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==92,"Two UI purchases cost eight 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("alchemy")));});
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("APTITUDES237_PASS: Vulkan FR/EN GUI 2/3/4, five themes, two real server toggles, ten permanent purchases, legacy preferences and merged Pause layout, two 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("aptitudes237-"+name);try{var dir=FabricLoader.getInstance().getGameDir().resolve("../../../../../build/aptitudes237-screenshots").normalize();Files.createDirectories(dir);Files.copy(source,dir.resolve(name+".png"),StandardCopyOption.REPLACE_EXISTING);}catch(Exception e){throw new AssertionError(e);}}
}
@@ -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);}
}
@@ -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.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,53 @@
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)->{});
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;
}
}
@@ -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"))) {
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)
@@ -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,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})
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;
}
}
@@ -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","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,80 @@
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 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")));
}
public static void register() {
java.util.Objects.requireNonNull(CraftAptitudes.ENCHANTING);
java.util.Objects.requireNonNull(CraftAptitudes.ALCHEMY);
PayloadTypeRegistry.clientboundConfiguration().register(Capability.TYPE,Capability.CODEC);
PayloadTypeRegistry.clientboundConfiguration().register(PermanentCapability.TYPE,PermanentCapability.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,41 @@
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());
private CraftAptitudes() {}
public static boolean target(String target){return target.equals("enchanting") || target.equals("alchemy");}
private static AttachmentType<Boolean> attachment(String target) {
return switch(target){case "enchanting"->ENCHANTING;case "alchemy"->ALCHEMY;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" : "";}
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,10 @@ 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,ProgressionPayloads.SwimmingCapability.TYPE)) {
handler.disconnect(label("swimming_version_required"));return;
}
@@ -522,6 +527,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 +545,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.",
@@ -2618,5 +2618,26 @@
"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."
}
@@ -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.",
@@ -2618,5 +2618,26 @@
"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."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

@@ -3,6 +3,9 @@
"package": "fr.koka.sanctuary.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"CraftAptitudeOpenMixin",
"CraftAptitudeMenuMixin",
"CraftAptitudeEnchantMixin",
"IslandChestLootMixin",
"BadlandsBands211Accessor",
"WestMonument217Accessor",
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.235"
version = "beta.237"
pack-format = "packwiz:1.1.0"
[index]