# 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.

### Priorités des serveurs et service takeout

Les quatre valeurs `task_preferences` d’un serveur (`take_order`, `serve`,
`bus`, `takeout`) sont sur une échelle commune de 0 à 5 et décident seules de
son choix entre les tâches disponibles. Les compétences déterminent ensuite la
vitesse ou la qualité d’exécution; ne pas les réinjecter comme priorités cachées.

Le chronomètre takeout appartient uniquement au client en tête arrivé au
comptoir. L’arrivée ou le départ d’un client derrière lui ne doit jamais le
réinitialiser. Sa barre affiche l’avancement du service; la patience takeout
reste invisible.

Le comptoir possède deux zones physiques permanentes et non superposées :
`takeout_prepare` cuisine, `takeout_service` aide le service de 30 %. **Le stock
ne change jamais la fonction d’une zone**, et la présence d’un employé ne bloque
jamais le joueur.

Le joueur n'a qu'une vitesse de déplacement. Le profil expose `move_speed` et
`move_stamina_per_second`; ne pas réintroduire de bascule marche/course, de
`toggle_run`, ni deux vitesses cachées. La valeur maître est 400 px/s, milieu
exact des anciennes valeurs 300/500.

Dans le restaurant, chaque `InteractiveProp` actif porte un tapis qui est aussi
la source de vérité de sa position et de sa portée. Le HUD reprend
`Player.current_target` et place le prompt Action près de cette cible; ne pas
réintroduire une seconde recherche de proximité dans le HUD. Les tapis ne
s'éclairent jamais à la sélection : les postes abordés latéralement utilisent
un tapis vertical, ceux abordés de face un tapis horizontal. Quand l'action est
possible, seul le meuble reçoit un rectangle de contour statique, large et
configurable via les propriétés `target_highlight_*`; les deux zones takeout
encadrent ainsi le comptoir physique, pas leur hit zone invisible. Le prompt est
une seule ligne transparente
et discrète : teinte neutre quand `can_interact()` est vrai, raison précise en
rouge quand un poste fixe est temporairement indisponible. Dans ce second cas,
Action appelle tout de même le poste afin que son refus soit aussi transmis par
le toast; `allows_unavailable_prompt()` réserve ce comportement aux cibles qui
savent expliquer leur blocage.

Le HUD ne porte ni liste permanente d'ingrédients, ni bloc d'assiette portée,
ni indicateur marche/course. Le stock vit dans le cinquième onglet Ingrédients
de la console. Le plat porté est nommé par un petit texte au-dessus du joueur.
Objectif courant, toast et fermeture du restaurant restent de petites lignes
dans le coin inférieur gauche, jamais des messages centraux.

---

## 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 22**, **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; tapis préparation jusqu'au plafond, cuisiniers sous X jusqu'à Y; tapis service +30 % quel que soit le stock, cumulable avec le +30 % d'un serveur; seconde file et 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 |
