# BIG EGG — directives de projet

Fichier chargé à **chaque** session : il doit rester court. Il sert de point de
reprise en cas de perte de contexte ou de reprise par un autre agent.

**Ce qui n'est pas ici** : le récit des fonctionnalités déjà construites,
l'historique des corrections, la justification détaillée des choix. Tout cela est
conservé mot pour mot dans `docs/journal-conception.md` (l'ancien fichier, 1400
lignes) et dans les quatre documents d'architecture. Les consulter avant de
rouvrir une décision.

**Règle de maintenance** : n'ajouter ici qu'une règle qui **change une décision
future**. Un bug corrigé n'y a sa place que si sa cause peut se reproduire. Tout
le reste va dans le journal.

Langue de travail : **français**. Identifiants, noms de fonctions et commentaires
de code restent en anglais, conformément au code existant.

---

## 1. Environnement

| | |
|---|---|
| Racine de travail | `E:\GDRIVE\SERVEUR ET PRODUCTION WEB\2025\REBIRTH\BIG EGG` |
| Projet Godot | `big-egg/` |
| Outil de données | `big-egg/tools/game-data-manager/` |
| Godot | `C:\Users\ELR17\Downloads\Godot_v4.7.1-stable_win64.exe\Godot_v4.7.1-stable_win64_console.exe` |
| PHP | `C:\wamp64\bin\php\php7.4.33\php.exe` |
| Node | `C:\Program Files\nodejs\node.exe` |

**Pas de dépôt Git.** Le filet est une copie datée dans `_backups/` avant chaque
chantier notable :

```bash
STAMP=$(date +%Y%m%d-%H%M) && DEST="_backups/big-egg_<motif>_$STAMP" \
  && mkdir -p "$DEST" && tar -cf - --exclude=.godot -C . big-egg | tar -xf - -C "$DEST"
```

Ne jamais tuer un éditeur Godot déjà ouvert. Préserver les fichiers existants.

### Génération d'art — serveur MCP PixelLab

Portée locale à ce projet (`~/.claude.json`). `create_character` puis
`animate_character`. Quatre choses à savoir, apprises au premier essai :

- **PixelLab rend des images séparées, jamais une feuille** (`frame_000.png`,
  `frame_001.png`…). Les composer en bande horizontale avant de les poser dans
  `assets/`, sinon la découpe `colonnes × rangées` n'a rien à découper.
- **Générer la vue sud seule** (`directions: ["south"]`) : `VisualHolder` ignore
  les directions (§6), donc les 4 ou 8 rotations sont du gâchis — 1 génération
  au lieu de 4.
- **Les URLs Backblaze directes renvoient 520.** Seul l'endpoint
  `…/characters/<id>/download` répond, avec l'en-tête d'autorisation.
- **L'éditeur ouvert importe tout seul au retour du focus** : déposer le fichier
  puis cliquer sur la fenêtre Godot suffit. Ne pas lancer un `--headless --import`
  concurrent.

---

## 2. Validation — à lancer après tout changement

```bash
# 1. Parse de tous les scripts et scènes
godot --headless --editor --path 'big-egg' --quit

# 2. Smoke test complet (le principal filet)
godot --headless --path 'big-egg' 'res://scenes/main/smoke_test.tscn'

# 3. Retour depuis l'arbre d'upgrades
godot --headless --path 'big-egg' 'res://scenes/main/upgrade_return_smoke.tscn' --quit-after 3

# 4. Outil
node --check big-egg/tools/game-data-manager/assets/app.js
for t in address localization render dive_editor room_editor delete_cascade \
         map_shape resync employee_editor visual_editor asset_layout enemy_bounds \
         upgrade_theme; do
  node "big-egg/tools/game-data-manager/tests/${t}_test.js" || echo "ECHEC $t"
done
# Après avoir ajouté une clé à Loc.UI_FALLBACKS:
node big-egg/tools/game-data-manager/tests/seed_ui_texts.js
php -l config.php ; php -l api.php ; php -l index.php
```

### Pièges d'exécution — coûteux si oubliés

- **Ne pas rediriger une longue commande Godot dans `grep`** : le pipe bufferise
  et la commande semble bloquée. Écrire dans un fichier puis filtrer.
- **Un nouveau script avec `class_name` doit passer par l'éditeur avant tout
  lancement headless**, sinon `Identifier "X" not declared`.
- Le smoke test tourne en ~6 s. Au-delà, c'est un blocage réel.
- Un échec isolé sur le dash ou la détection ennemie (sensibles au timing
  physique) se revérifie par une seconde exécution avant d'être traité comme une
  régression.
- **Contrôler le code de sortie d'une suite JS, pas son texte** : une exception
  n'imprime aucun « ECHEC » et un `grep` la ferait passer pour verte.

---

## 3. Architecture — invariants

### Espace d'adressage

Toute valeur se lit par une adresse en points. Racines : `fact`, `stat`,
`player`, `run`, `meta`, plus les noms de collections du fichier maître
(`enemies`, `ingredients`, `recipes`, `upgrade_nodes`, `customer_profiles`,
`attack_patterns`, `movement_patterns`, `player_configs`, `employees`,
`visuals`, `rooms`, `dimensions`, `biomes`, `room_elements`).

```
fact.boss_defeated                 stat.attack_damage
enemies.fungling.total_killed      recipes.fried_egg.can_cook
```

**Lecture uniforme, écriture étroite** : tout se lit pareil, seules les *cellules
stockées* sont inscriptibles. Une valeur calculée a un résolveur, pas de setter.

| Nature | Origine | Écrivable | Sauvegardé |
|---|---|---|---|
| Donnée d'auteur | JSON maître | non | non |
| Compteur vivant | événements de jeu | oui | oui |
| Valeur calculée | fonction | non | non |

`AddressCatalog` (scripts/core) est la source de vérité des racines, champs
vivants, champs calculés et crochets. `Registry` résout lectures/écritures et
applique les effets déclaratifs. **Ne pas ajouter de moteur de règles central** :
le registre est un annuaire, chaque système garde sa logique.

### Statistiques — quatre couches

```
final = (base + Σflat) × (coef_base + Σscaling) × (1 + Σglobal) × Π override
      → minimum → maximum → arrondi
```

`global` est **séparé de `scaling`** pour que les effets temporaires ne soient
pas dilués par la progression (une malédiction à −0,5 fait toujours
moitié-dégâts). `override` est un vrai multiplicatif isolé, rare.

- **La liste de modificateurs est la vérité, l'accumulateur est un cache.** Ne
  jamais muter une valeur finale : elle ne saurait pas se dé-muter et ne
  survivrait pas à une sauvegarde. **Un champ qui met une statistique en cache
  doit s'abonner à `state_changed`.**
- Sources nommées `<type>:<id>` (`upgrade:`, `equipment:`, `consumable:`,
  `debuff:`, `set:`, `run:`, `employee:`, `test:`). Retrait par
  `remove_modifiers_by_source()`.
- **Ordre imposé** : `set` → `add` → `multiply`, puis tri par source. Sans lui le
  résultat dépendrait de l'ordre d'achat du joueur.

### Effets temporaires

`EffectHolder` sur chaque entité. Une définition combine modificateurs de
statistiques, actions périodiques et drapeaux de capacité (`can_move`,
`can_attack`, `can_interact`).

- **La paralysie est un drapeau, pas une vitesse nulle** : une vitesse à zéro
  n'empêche ni d'attaquer ni de finir une animation.
- Une instance par application, groupées par `stack_key`. `stack` = piles
  indépendantes, `stack_refresh` = expiration commune. Défaut de clé :
  l'identifiant de l'effet (seul cas où `max_stacks` plafonne vraiment).
- **Les instances ne sont pas adressables** : seuls les agrégats remontent.
- Les effets du joueur vivent dans `GameState.player_effects`, réhydratés après
  chaque changement de scène.

### Effets déclaratifs et crochets

Crochets par collection : `on_kill`, `on_spawn`, `on_damage_player`, `on_hurt`,
`on_cooked`, `on_served`, `on_collected`, `on_purchase`, `on_hired`,
`on_left_unserved`, `on_blocked`, `on_taken`.

Un effet est un enregistrement **plat** : `{op, path, value}` avec
`op ∈ {set, add, multiply, min, max, toggle}`. **Aucune expression, aucune
condition imbriquée** — sinon c'est un langage de script, et un mauvais. Ce qui
ne rentre pas reste en GDScript.

`Registry.run_hook(collection, id, hook, origin)` porte à la fois ce que
l'événement change et l'effet visuel qu'on en voit. Un seul point d'appel.

### Réactivité et cohérence

- Recalcul global via `state_changed`. **Pas d'invalidation fine par chemin** : à
  cette échelle elle n'apporte rien et ajoute des abonnements fantômes.
  `Registry.write()` reste le point de passage unique si ça change un jour.
- Contrôles à l'ajout et au chargement, oui. **Pas de balayage périodique** : un
  processus qui « répare » l'état masque les bugs et les rend intermittents.

### Apparence de l’arbre d’upgrades

`upgrade_themes` est l’unique autorité des formes, couleurs et segments par état.
Un nœud ne porte que `theme`, son éventuel `image_path` et
`show_parent_link`. **Ne pas réintroduire `appearance` ou `state_styles` par
nœud** : une variation réutilisable est un thème.

---

## 4. Textes multilingues — **directive active**

> **Tout nouveau texte affiché passe par un identifiant.** Jamais de littéral
> dans le code.

- Interface : clé dans `Loc.UI_FALLBACKS` (`scripts/core/loc.gd`) puis
  `Loc.ui("ui.domaine.nom")`.
- Entité : `DataCatalog.ingredient_name()`, `recipe_name()`, `enemy_name()`,
  `customer_name()`, `upgrade_name()`, `variable_name()`.
- Puis lancer le semis, ou ajouter la clé via l'onglet **Textes**.

**La clé de traduction est une adresse** (`ingredients.egg.display_name`,
`ui.restaurant.plates`) : le catalogue se dérive des données, aucun registre
manuel. Collections traduisibles déclarées dans `Loc.TEXT_FIELDS`, miroir de
`localization_text_fields()` (PHP) et `LOCALIZATION_TEXT_FIELDS` (JS).

Résolution : `langue courante → repli (en) → défaut d'auteur → texte du code → la
clé`. Le **défaut d'auteur** est le texte saisi dans l'outil ; il n'est pas
destiné à être livré (la version finale active `Loc.strict_mode`).

**Supprimer une entité emporte ses textes** (`dropLocalizationOf`), sinon
l'onglet Textes garde des lignes qui ne nomment plus rien.

Langues : `fr`, `en`. **Reste à migrer** : `hud.gd` (~9 textes), l'arbre
d'upgrades (~6). `dungeon.gd` est migré.

---

## 5. Synchronisation des données

En changeant le schéma, garder alignés :

1. `DataCatalog.SCHEMA_VERSION` (Godot)
2. Valeurs de secours intégrées de `DataCatalog`
3. Normalisation / validation Godot
4. `AddressCatalog` (racines, champs vivants, calculés, crochets)
5. `Loc.TEXT_FIELDS` si des textes sont concernés
6. `data/game_data_master.json`
7. `tools/game-data-manager/data/draft.json`
8. `DATA_SCHEMA_VERSION` (PHP)
9. Normalisation PHP (miroir de la validation Godot)
10. Champs de l'éditeur JS
11. Tests de régression
12. Ce fichier et le `README.md` de l'outil

Versions : **schéma 20**, **sauvegarde 11**. Contenu : 5 ingrédients, 5 plats,
5 monstres, 3 visuels, 3 profils clients, 7 biomes, 5 dimensions, 8 récompenses,
6 éléments de salle, 39 statistiques, 27 nœuds d'upgrade, 6 thèmes d’upgrade,
5 employés, 8 facts.
Les salles sont écrites par l'utilisateur.

**Page blanche assumée (18/08/2026)** : salles et gardiens supprimés, réécrits
case par case. Godot retombe alors sur `_generated_rooms` (4 cases par dimension,
la dernière clôturante). **Dès qu'une seule salle est écrite, le filet ne
s'applique plus** : une case sans `ends_dimension` rend la plongée interminable.
L'outil le signale.

**Godot valide indépendamment** : une donnée invalide produit un avertissement et
bascule sur les valeurs de secours plutôt que de planter. Préserver la
compatibilité des sauvegardes quand c'est praticable.

---

## 6. Carte du construit

| Domaine | État | Détail |
|---|---|---|
| Espace d'adressage, statistiques, effets | construit | `docs/architecture-variables.html` |
| Combat, patterns d'attaque et de mouvement | construit | §7 ci-dessous |
| Donjon, plongées, salles, dimensions | construit | `docs/architecture-donjon.html` |
| Trottoir, file d'attente, tables | construit | journal, §5d |
| Postes, console, brigade — **9 phases** | construit | `docs/architecture-employes.html` |
| Habillage visuel — **phases 1 à 6** | construit | `docs/architecture-visuels.html` |
| Équipement (arme, armure, sets, skills) | **non construit** | socle seulement |

### Trois portées de persistance — le point structurant

| Portée | Contient | Effacée |
|---|---|---|
| `run` | position dans la plongée, dimension figée, récompenses | à chaque retour au restaurant |
| `dimension` | upgrades générales, pièces, ingrédients, compteurs, restaurant | à la victoire sur le gardien de fin |
| `meta` | upgrades spéciales, `dimension_index`, `gate_pending`, débloqué | jamais |

La portée est **déclarée par la donnée** (`scope`), jamais codée en dur. Défaut
`dimension` : un oubli ne rend jamais un acquis permanent par accident.
`GameState.end_run()` et `complete_dimension()` sont les deux seules opérations.

Cycle : dimension 1 (16 cases) → 2 (21) → 3 (26) → 4 (31, case 26 sous
`fact.master_fork_owned`) → retour à la première. L'épreuve `fork_trial`
(5 cases) accorde la fourchette.

### Écrit, publié, mais **inerte** — à savoir avant de promettre quoi que ce soit

- **`room_elements` et `behaviours`** : validés et publiés, le jeu ne les fait ni
  apparaître ni ne les interprète.
- **Compétences d'employé `stamina`, `poise`, `thrift`, `plating`, `nose`** :
  éditables des trois côtés, **aucun code ne les lit**. `tray` non plus — un
  serveur porte une assiette à la fois.
- Le cuisinier tient les postes `stove` et `takeout` selon leur priorité :
  **l'épicerie reste manuelle**.
- La production hors ligne ne couvre que les **ventes du stock takeout déjà
  cuisiné**, pas la production de la brigade.
- **La plongée ne rapporte pas de pièces** : le volet monétaire de l'assurance
  est implémenté mais jamais sollicité.
- L'habillage du restaurant **par dimension** reste à faire.
- **`VisualHolder` ignore les directions** : il joue toutes les cases d'une
  feuille comme une seule animation, `face()` ne fait qu'un miroir horizontal.
  Poser une feuille directionnelle ferait défiler les orientations comme un cycle
  de marche. Support directionnel = une rangée choisie par l'orientation + un
  champ de schéma des trois côtés. **À décider avant de générer des animations
  par direction** (elles coûtent une génération par direction).

---

## 7. Invariants de conception — ne pas casser

**Ces règles se sont payées cher. Les enfreindre casse quelque chose qu'aucun
test ne rattrape forcément.**

### Données et autorité

- **Zéro ou vide sur une salle veut dire « demande au-dessus ».** Effectif à 0 →
  statistique. Table de monstres vide → table du biome, puis tout le bestiaire.
  Deux niveaux seulement : la salle, puis le défaut global — le biome n'entre
  dans aucun calcul, ce n'est qu'une **palette** (nom, description, 3 couleurs,
  ambiance).
- **Le repli doit être visible là où on écrit** : le formulaire d'une case
  affiche ce qu'elle héritera, la grille marque d'un `∅` ce qui n'a ni effectif
  ni table.
- **La salle est la seule unité écrite.** La dimension n'est qu'une colonne
  nommée. Le boss est une créature posée dans une case qui coche
  `ends_dimension` — exactement une par colonne. Le verrou n'existe pas comme
  mécanique : c'est une `conditions` non remplie + un crochet `on_blocked`.
- **Deux choses sont figées dans `run`** : la dimension (sinon franchir une porte
  après un gain changerait de colonne en cours de plongée) et la salle (sinon
  chaque relecture retirerait au sort le remplacement par une alternative).
- Sorties : `exit_level` prime sur `exit_offset` ; au-delà de la colonne = fin de
  plongée. Désignation des cases **positionnelle**, renumérotée à la suppression.
- Salles alternatives : la probabilité la plus **haute** décide s'il y a
  remplacement, puis répartition au poids. **Dépasser 100 % n'est pas une
  erreur** ; ajouter une alternative dilue les autres au lieu d'augmenter la
  fréquence des spéciales.
- **Suppression : cascade** sur une entrée de liste ou de carte, **blocage** sur
  un champ obligatoire à valeur unique. Une carte se compare par ses **clés**
  (`CASCADE_MAPS`) *ou* par ses **valeurs** (`CASCADE_VALUE_MAPS`) — un visuel
  est une valeur. Un refus doit dire **par où sortir** (`refuseDeletion`).
- **Une adresse se compare avec son point** : `fact.boss_defeated.` seul
  laisserait passer l'adresse nue, sans point emporterait `fact.boss_defeated_bis`.
- **L'outil ne voit pas ce que le code lit par son nom** (`table_count`,
  `boss_defeated`) : supprimer une statistique ou un fait passe toutes les
  vérifications et casse quand même le jeu. La confirmation le dit, faute de
  pouvoir le vérifier.
- **Un champ d'auteur que personne ne lit finit par mentir** — le supprimer.

### Physique, visuel, agents

- **Un visuel ne décide jamais de la collision.** `ShapeFactory` et
  `body_radius()` gardent physique et portée ; un visuel n'a qu'un décalage et
  une échelle. Sinon chaque retouche d'art devient une session d'équilibrage.
  **La réciproque tient aussi** : la créature pose `size_multiplier` sur
  `visuals.scale`, qui se multiplie avec celui du visuel. `size` dit quelle
  taille fait la bête, `scale` cale une image donnée sur un corps de référence.
- **La portée se mesure au corps de la cible, jamais à son centre**, et **depuis
  le centre de l'attaquant** — jamais corps à corps, sinon la règle cesse d'être
  symétrique. `Enemy.body_radius()` = 18 × `size_multiplier`,
  `Player.body_radius()` = 19. L'angle se compare en retirant la demi-ouverture
  angulaire du corps. La **détection** reste au centre : c'est de la vue.
- Couches physiques : joueur `1`, ennemis et murs `2`, ramassables `4`.
- La géométrie d'attaque vit dans des **patterns réutilisables**, pas dans la
  fiche du monstre. Chaque état a un chemin de fin explicite, sinon un visuel
  reste accroché.
- **Un agent qui agit à distance téléporte.** Une tâche de portage a deux temps :
  aller chercher, puis porter. Quand un poste produit quelque chose, il le pose
  **là où il est**.
- **Un objectif se vérifie pendant le trajet, pas à l'arrivée**
  (`_task_still_valid()` à chaque frame).
- **Une réservation doit couvrir la ressource visée, pas seulement la tâche** :
  la ligne d'ardoise réservée sans le four faisait converger deux cuisiniers.
- **Un agent sans tâche a quand même un comportement** : il regagne son poste
  d'attente.
- **Aucune coordination centrale** : chacun note les tâches disponibles pour lui
  et réserve ce qu'il vise.
- **Un seul point de passage par geste.** `pick_up` et `carry_by` faisaient le
  même geste et un seul encaissait : le paiement appartient au ramassage
  (`Dish._on_dirty_plate_taken`), pas au porteur.
- **Toute phase de la brigade est un déverrouillage** (compte à 0 = « ce poste
  n'existe pas encore »), **sauf la console** (`management_console`, base 1) :
  c'est le tableau de commande du magasin, pas un poste de travail.
- Courbe visée : celle des jeux d'accumulation. Compétences **multiplicatives et
  sans plafond dur** — à terme la brigade dépasse le joueur de plusieurs ordres
  de grandeur.
- **Ce qui n'est pas capturé au changement de scène disparaît sans un mot** :
  assiettes, convives, file, position des employés, cuissons en cours. En
  ajoutant un état à la salle, se demander tout de suite qui le rejoue au retour.

---

## 8. Pièges — durables

### Godot / GDScript

- **`action` est aussi lié au clic gauche** (`project.godot`). `Node._input()`
  tourne **avant** le routage GUI : toujours laisser passer les
  `InputEventMouseButton`, sinon aucun bouton ne reçoit le clic.
- **Une garde `is_instance_valid()` dans une boucle typée ne protège pas** :
  l'assignation de `for x: Type in ...` échoue avant le corps. Une entité qui
  s'inscrit dans une liste s'en retire elle-même dans `_exit_tree()`.
- **Une lambda GDScript capture par valeur** : sonder un signal demande un
  conteneur partagé, pas une variable locale.
- **Un `z_index` négatif sur un enfant le fait passer sous son parent**
  (`z_as_relative` vrai par défaut). `visible` ne prouve rien, c'est le `z` qu'il
  faut contrôler.
- **Un nœud retiré par `queue_free()` reste compté jusqu'à la fin de la frame** :
  `remove_child()` d'abord, libération ensuite.
- **Un nœud qui n'affiche qu'un statut conditionnel doit redessiner une fois de
  plus au passage à vide**, sinon le dernier texte reste gravé.
- **Un `call_deferred("_focus_...")` vole le focus à la frame suivante.**
- **Godot n'exécute que ce qu'il a importé** : un fichier sans `.import` voisin
  passe toutes les validations de données et ne s'affiche pas. (Les `.tscn` et
  `.gdshader` sont natifs : ils n'ont pas de `.import`, c'est normal.)

### PHP / JS / outil

- **PHP encode un tableau associatif vide en `[]`, pas `{}`** — et Godot refuse
  `[]` là où il attend un dictionnaire. Le fichier peut contenir `"rooms": {}` et
  `api.php` le relire en tableau vide puis le renvoyer en `[]` : le défaut
  n'apparaît que quand une collection est **vide au chargement**, et tout semble
  se publier « avec succès ». `json_map()` avant publication ;
  `restoreMapShapes()` rétablit la forme à la lecture, pour les collections **et**
  leurs sous-cartes. Un normaliseur doit rendre une carte encodable
  (`normalize_amount_map`, `normalize_visual_map`) — et ne pas être ré-enveloppé
  dans `json_map()`, ce qui lève une TypeError. Côté JS : ne jamais écrire une
  clé texte sur un tableau, `JSON.stringify` la supprime en silence.
- **Publier depuis l'outil écrase les écritures fichier** : le navigateur publie
  son état **en mémoire**. Une page ouverte avant une modification faite hors de
  l'outil la réécrit à la publication, en silence et sans conflit visible.
  **Cliquer « Recharger du serveur » avant de publier** dès que les données ont
  été touchées ailleurs.
- **Les assets de l'outil sont versionnés par `filemtime`** dans `index.php`.
  Sans ça le navigateur sert du cache et une correction publiée reste invisible :
  on croit le code faux alors qu'il ne s'exécute pas.
- **Une valeur hors bornes se ramène à la borne la plus proche**
  (`clamp_to_bounds`), pas au minimum — sinon `max_size: 8` donne 0,25, l'inverse
  exact de l'intention et de ce que fait `clampf`. L'erreur reste signalée.
  **Le plafond de taille (0,25–5) n'a aucune justification écrite** : c'est un
  garde-fou miroir des trois côtés, pas une limite technique. Le relever = trois
  lignes plus le miroir dans `enemy_bounds_test`.
- **Masquer un enfant de grille CSS décale les autres d'une piste** : une piste
  masquée se **retire** du `grid-template-columns`, elle ne se met pas à zéro.
- **Un compte rendu de publication doit compter ce qu'on vient d'éditer**, sinon
  une publication qui perd tout dit quand même « succès ».

### Environnement

- **Le dossier de travail est sur Google Drive.** Une synchronisation a rétabli
  un fichier dans une version antérieure **en conservant sa date** — donc
  invisible à `ls`. Comparer au dernier `_backups/` avant de conclure qu'une
  correction n'a jamais été appliquée.
- **Un script de correction qui écrit à la fin n'applique rien s'il échoue au
  milieu** : vérifier ce qui a réellement été écrit avant de relancer.
- **Un `.replace(..., 1)` peut viser la mauvaise occurrence** (`if coins < cost:`
  existe dans `purchase_upgrade` **et** `hire_employee`). Vérifier l'unicité du
  motif avant de conclure qu'un test est creux.
- **Une insertion automatique peut tomber dans la fonction voisine** : le fichier
  compile et se lit comme un miroir correct. Contrôler que chaque appel
  appartient bien à la fonction qui porte son nom.

### Tests — les six règles qui comptent

1. **Isoler l'état persistant.** Le smoke test purge assiettes, compteurs, menu,
   effets, `meta_values`, et **tout** modificateur `run:` et `test:` — un
   reliquat d'une exécution interrompue fausse des contrôles sans rapport.
   `GameState.save_locked` se pose **au démarrage** (dans `_enter_tree()` pour
   `upgrade_return_smoke`, le `_ready` d'un enfant passant avant celui du
   parent), et la copie de sauvegarde se prend dans `load_game()` — pas champ par
   champ, mais **le fichier octet pour octet** (`save_file_at_boot`) : énumérer
   les champs a déjà versé deux employés de test dans la partie du joueur.
2. **Imposer ce dont on dépend, ne rien supposer.** Le pattern d'attaque, la
   distance relative à `attack_range`, la statistique plutôt que les upgrades, sa
   propre colonne de salles plutôt que le contenu publié, une vie plancher avant
   un `take_damage`, le bonus qu'on prétend vérifier, une mise de départ avant de
   mesurer une perte bornée à zéro, de quoi cuisiner avant d'attendre un client.
   **Ne jamais figer un numéro de case** ni **choisir une entité par index** —
   sélectionner par capacité et écarter les instances inactives. **Figer** ce qui
   bouge tout seul (une créature erre d'elle-même).
3. **Tester le geste du joueur, pas la fonction.** `toggle_open_closed()` et
   `hire_employee()` étaient écrits, testés, et injoignables : aucun bouton ne
   les atteignait. De même, chercher un nom dans un source ne teste pas son
   exécution — vérifier la fonction *et* la forme exacte de son appel.
4. **Vérifier que le test attrape vraiment la régression** en cassant
   temporairement le code. Un test qui passe sans avoir jamais échoué ne prouve
   rien. La forme cassée doit être **unique dans les deux sens** et **jamais
   vide** (`s.count('')` n'est jamais 1 : le fichier reste cassé en silence), et
   la série doit **vérifier que l'arbre est revenu** après chaque annulation.
5. **Casser la donnée publiée, pas la valeur de secours.**
   `DataCatalog.GAME_VARIABLES` est écrasé par `data/game_data_master.json` : le
   piège s'est produit deux fois.
6. **Un test qui lit une référence nulle bloque au lieu d'échouer** (timeout,
   aucun rapport) — garder les gardes. Ne **jamais appeler
   `complete_dimension()` au milieu du smoke test** : il vide économie et
   upgrades et avance le cycle, tout ce qui suit s'effondre.

---

## 9. Méthode de travail attendue

- Inspecter les fichiers réels avant d'agir ; ne pas se fier à un instantané de
  conversation.
- Préférer les implémentations pilotées par les données.
- Garder souris **et** clavier/manette fonctionnels. Échelle logique 1920×1080.
- Ajouter une couverture de test proportionnée, puis la vérifier en cassant le
  code (§8, règle 4).
- **Rapporter exactement ce qui a changé, ce qui a cassé en route, et les limites
  restantes.** L'utilisateur ne peut pas repérer les régressions lui-même : ne
  pas présenter uniquement le résultat final.

### Décisions arrêtées — ne pas rouvrir sans raison

| | Décision |
|---|---|
| Collections | noms du fichier maître, inchangés |
| Adresses | pas de préfixe `element.` |
| Couches | `flat`, `scaling`, `global`, `override` |
| Empilement | clé par effet par défaut |
| Réactivité | recalcul global |
| Compteurs | `fact_values` fait foi, `statistics` supprimé |
| Dimensions | 4 dans le cycle, plus `fork_trial` hors cycle |
| Récompenses | source `run:`, sans remise dans une offre, avec remise entre deux |
| Salles | la salle est l'unité écrite, le biome n'est qu'une palette |
| Boss | une créature du bestiaire posée dans une case qui coche `ends_dimension` |
| Verrou | `conditions` de case + crochet `on_blocked`, jamais codé en dur |
| Sortie | `exit_level` prime sur `exit_offset` ; au-delà de la colonne = fin |
| Cases | désignation positionnelle, renumérotées à la suppression |
| Remplacement | le maximum déclenche, la somme ne fait que répartir |
| Zéro/vide | veut toujours dire « demande au-dessus » |
| Suppression | cascade sur une entrée de liste ou de carte, blocage sur un champ obligatoire |
| Employés | portée déclarée par la fiche (défaut `dimension`), aucun coût continu |
| Comptoir takeout | recette choisie à la console; joueur jusqu'au plafond, cuisiniers sous X jusqu'à Y; décision et seconde file devant le comptoir; service automatique sans vaisselle (30 s de base) |
| Visuels | collection `visuals` partagée, animations en feuille + grille, décor en image de fond |
| Rangement | par type (`creatures/`, `characters/`, `props/`, `items/`, `backgrounds/`, `effects/`, `shared/`) — propose, n'interdit jamais |
| Shaders | matériau posé sur un visuel, formulaire déduit des `uniform` |
| Restaurant | habillé **par données** d'une dimension à l'autre, pas une scène par dimension |
| Équipement | prévu (arme, armure, objet, consommables, 1–3 skills, sets resto/donjon) mais **non construit** |

---

## 10. Documents détaillés

| | |
|---|---|
| `docs/journal-conception.md` | **archive intégrale** de l'ancien CLAUDE.md : récits de construction, historique des corrections, justifications |
| `docs/architecture-variables.html` | espace d'adressage, statistiques, effets |
| `docs/architecture-donjon.html` | plongées, salles, dimensions, portées |
| `docs/architecture-employes.html` | les neuf phases de la brigade |
| `docs/architecture-visuels.html` | habillage visuel, six phases |
