# BIG EGG — directives de projet

> ## AVANT TOUTE CHOSE — quel dossier travailler
>
> **La branche de travail est `big-egg-3d/`.** Tout développement, toute
> correction, tout nouveau contenu s'y font. C'est elle qui tourne.
>
> **`big-egg/` est une RÉFÉRENCE, en lecture seule.** On l'ouvre pour comparer,
> pour retrouver une version antérieure d'un fichier, pour comprendre une
> décision. **On n'y écrit pas.** Une correction faite là est une correction
> perdue.
>
> Cette règle **prime sur toute autre indication de ce fichier** et sur toute
> habitude prise dans les sessions précédentes. Elle vaut pour tout agent, quel
> qu'il soit. Décidée par l'auteur le 27/08/2026.
>
> `big-egg-3d/` **contient le jeu entier** — mêmes scripts, mêmes données,
> mêmes règles — plus un pont qui lui donne un rendu 3D. Ce n'est ni une
> maquette ni un sous-ensemble: le smoke test du jeu y tourne sans modification.
> Voir `big-egg-3d/LISEZMOI-3D.md`.

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 actif** | **`big-egg-3d/`** — le jeu entier + rendu 3D; voir `LISEZMOI-3D.md` |
| Référence, lecture seule | `big-egg/` — ancienne branche 2D. **Ne rien y écrire.** |
| Outil de données | `big-egg-3d/tools/game-data-manager/` |
| Outil des formes 3D | `big-egg-3d/tools/shapes-3d-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-3d_<motif>_$STAMP" \
  && mkdir -p "$DEST" && tar -cf - --exclude=.godot -C . big-egg-3d | tar -xf - -C "$DEST"
```

`big-egg/` ne bouge plus: il est déjà dans les `_backups/` datés d'avant le
27/08/2026 et n'a plus besoin d'être copié à chaque chantier.

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`. Le serveur parle aussi en JSON-RPC direct sur
`https://api.pixellab.ai/mcp` (en-tête `Authorization: Bearer`), ce qui permet de
scripter tout un lot sans passer par les outils MCP de la session.

- **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.
- **Animer la vue sud seule** (`directions: ["south"]`) : `VisualHolder` ignore
  les directions (§6). La création `mode: "v3"` produit toujours 8 rotations,
  pour environ 2 générations à ces tailles: on n'en garde que la sud.
- **`frame_count: 6` rend 7 images** : la pose de départ est conservée comme
  image 0. Le `count` du visuel suit le nombre réellement composé.
- **Les URLs Backblaze répondent, mais refusent une requête sans `User-Agent`**
  (401), et un fichier tout juste publié peut mettre quelques secondes à
  répondre : en-tête explicite plus deux ou trois réessais.
- **Dix travaux simultanés au maximum** (abonnement Tier 2), et une création v3
  compte plusieurs travaux internes. Une boucle qui relit `list_jobs` et relance
  ce qui est libre traite 48 créatures en une heure environ.
- **Une créature n'est finie qu'avec sa marche** : la vue sud arrive bien avant
  l'animation. Un pipeline qui classe l'entrée dès la vue sud perd la marche.
- **Une marche peut revenir incomplète, et une relance crée un second groupe.**
  Le service rend direction par direction et l'une peut manquer; il faut
  redemander les manquantes en passant `animation_group_id`. Sans lui, le
  personnage se retrouve avec **deux animations du même nom**, chacune portant
  une partie des directions. Un lecteur qui indexe par nom laisse le second
  groupe écraser le premier, croit une direction manquante et la redemande sans
  fin. Fusionner les groupes homonymes à la lecture.
- **L'échelle d'un visuel se mesure, elle ne se devine pas** : prendre la hauteur
  réellement dessinée (hors marge transparente) et la ramener au corps de
  référence — `18 × size_multiplier` de rayon, donc 36 px de diamètre à
  l'échelle 1, plus un débord d'environ 1,6. Elle ne doit dépendre que de
  l'image: le multiplicateur de taille de la fiche se multiplie ensuite.
- **Se mesurer sur la même pose des deux côtés — la case sud, image 0.** Le
  service rend chaque animation sur le canevas qu'il lui faut: un repos tient
  dans 160 px, une marche demande 224 px pour loger le mouvement. Mesurer le
  canevas, ou prendre la plus grande case de la feuille (une jambe tendue en
  marche, la bête au calme au repos), donne deux références incomparables et la
  créature **change de taille en changeant d'état**. Sur les clients l'écart
  atteignait 133 %. La case sud au repos vaut ~155 px dans toutes les feuilles
  d'un même personnage: elle est la seule mesure qui s'accorde.
- **Remplacer une image sans remesurer son échelle est le pire des deux
  mondes** : l'ancienne échelle appliquée au nouveau dessin donnait 186 px
  debout contre 132 px en marchant. Toute refonte d'asset se termine par une
  remesure et une republication, jamais par la seule écriture du PNG.
- **Un objet à plusieurs faces se demande en une fois** : quatre images libres
  donnent quatre meubles différents posés sur un bout de sol.
  `create_8_direction_object` rend le *même* objet sous huit angles (~25
  générations, carré, 24 à 168 px) — c'est ce qui remplit `front/right/back/left`.
- **Le poids d'une image libre est d'une génération** (`create_image_pixflux`,
  10 à 40 s, jusqu'à 400 px, `no_background`): c'est l'outil des icônes et des
  plats, pas `create_1_direction_object` qui en coûte 20 à 40.
- **L'éditeur ouvert importe tout seul au retour du focus**, et un lancement
  `--headless` du jeu importe aussi ce qui manque au passage. Ne pas lancer un
  `--headless --import` concurrent. Sans import, le smoke test le dit
  franchement: « Le visuel publié « X » n'est pas chargeable ».

---

## 2. Validation — à la mesure du changement

**On ne relance pas toute la batterie après chaque modification.** Elle coûte
plusieurs minutes et beaucoup de contexte pour, la plupart du temps, revérifier
ce que la modification ne pouvait pas atteindre. On monte l'échelle jusqu'au
niveau du changement, et on s'arrête là.

| Niveau | Ce qui a changé | Ce qu'on lance |
|---|---|---|
| **0** | documentation, commentaires, `_backups/` | rien |
| **1** | un script, sans toucher aux données ni aux règles | **le seul contrôle qui couvre la zone** (voir la carte ci-dessous) |
| **2** | une règle de jeu, une entité, une interaction, un texte | smoke test **+** le contrôle de la zone |
| **3** | schéma, fichier maître, outil de données, autoload, `project.godot` | **tout**, dans l'ordre du bloc ci-dessous |

En cas de doute entre deux niveaux, prendre le plus haut **une fois**, pas à
chaque itération d'un même chantier: on itère au niveau 1 et on passe le niveau
supérieur quand le chantier est fini.

**Quelle zone, quel contrôle:**

| Zone touchée | Contrôle |
|---|---|
| pont 3D, avatars, caméra, champs d'attaque, butin affiché | `bridge_3d_check` |
| menu, changement de scène, clavier/manette | `bridge_3d_flow_check` |
| règles du jeu, salle, donjon, brigade, économie | `smoke_test` |
| persistance à travers un changement de scène | `upgrade_return_smoke` |
| éditeur de données (JS) | le test JS du même nom |
| validation PHP, publication | le test PHP du même nom |
| formes 3D | `mirror_test.php` |

**Un test neuf se vérifie en cassant le code** (§8, règle 4) — une fois, à sa
création. Ce n'est pas un rituel à rejouer ensuite.

Toutes les commandes visent **`big-egg-3d`**, la branche de travail.
```bash
# 1. Parse de tous les scripts et scènes (et enregistrement des `class_name`)
godot --headless --editor --path 'big-egg-3d' --quit

# 2. Smoke test complet — les RÈGLES du jeu, inchangées sur cette branche
godot --headless --path 'big-egg-3d' 'res://scenes/main/smoke_test.tscn'

# 3. Retour depuis l'arbre d'upgrades
godot --headless --path 'big-egg-3d' 'res://scenes/main/upgrade_return_smoke.tscn' --quit-after 3

# 4. Le pont 3D — avatars, dessin 2D éteint, caméra, suivi
godot --headless --path 'big-egg-3d' 'res://scenes/main/bridge_3d_check.tscn'

# 5. Le parcours réel — menu, changement de scène, clavier
godot --headless --path 'big-egg-3d' 'res://scenes/main/bridge_3d_flow_check.tscn'

# 6. Outil de données
node --check big-egg-3d/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 speech_bubble story_scene drop_editor drop_magnet \
         management_layout restaurant_element recipe_condition; do
  node "big-egg-3d/tools/game-data-manager/tests/${t}_test.js" || echo "ECHEC $t"
done
# Après avoir ajouté une clé à Loc.UI_FALLBACKS:
node big-egg-3d/tools/game-data-manager/tests/seed_ui_texts.js
php big-egg-3d/tools/game-data-manager/tests/speech_bubble_validation_test.php
php big-egg-3d/tools/game-data-manager/tests/story_scene_validation_test.php
php big-egg-3d/tools/game-data-manager/tests/drop_validation_test.php
php big-egg-3d/tools/game-data-manager/tests/recipe_condition_validation_test.php
php big-egg-3d/tools/game-data-manager/tests/restaurant_element_validation_test.php

# 7. Le miroir de l'outil des formes 3D
php big-egg-3d/tools/shapes-3d-manager/tests/mirror_test.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`, `story_scenes`).

```
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.

Les quantités ponctuelles utilisent elles aussi des adresses :
`player.coins` pour les pièces et `ingredients.<id>.owned` pour le stock réel
d'un matériau. `add` crédite, une valeur négative retire sans jamais descendre
sous zéro. `total_collected` reste un compteur historique distinct.

`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. Les deux zones sont les deux moitiés contiguës et disjointes
de la **face accessible** du comptoir, chacune avec sa couleur.

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.

**Les tapis d'interaction n'existent plus.** Chaque `InteractiveProp` actif
déclare un **masque de faces utilisables** (`usable_faces`, drapeaux
`Top/Right/Bottom/Left`) et dessine sur chacune une **bordure intérieure** dont
la profondeur vaut `face_band_ratio` (0,15 par défaut) de la dimension
**perpendiculaire** à la face. Cette bordure est la zone d'interaction : elle
est *dans* le meuble, donc elle ne peut jamais recouvrir un personnage — il n'y
a plus de couche de sol à ordonner. Couleur et épaisseur sont configurables dans
le groupe d'inspecteur `Usable Faces`. Masque vide = ancien comportement, un
simple rayon autour de `interaction_offset` : c'est ce qui laisse le donjon
intact. Répartition en place : four face droite, console face basse, évier,
poubelle et étagère les quatre faces.

`interaction_offset` ne décide plus de la portée; il déclare **par quel côté on
aborde le meuble par défaut**, ce que rend `default_stand_position()`.
`get_interaction_position()` suit la face la plus proche du joueur, écartée de
`face_stand_gap` (26 px, l'écart des anciens tapis). 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. Quand l'action est
possible, seul le meuble reçoit un contour statique **collé à son bord** (marge
0 par défaut), configurable dans le groupe d'inspecteur `Target Outline` 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 : un verbe court quand `can_interact()` est vrai, puis
il disparaît pendant que la touche Action est maintenue. Une raison précise
reste visible en rouge quand un poste fixe est temporairement indisponible,
même si Action est maintenue. 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.

La portée physique reste toujours le premier filtre d'une interaction. Si deux
cibles admissibles ont un score de proximité séparé d'au plus 32 px, le curseur
départage : il vise la **face dessinée** de l'une d'elles, ou le tapis invisible
juste devant elle (`face_apron_rect`, de la profondeur de `face_stand_gap`).
**Quand le curseur ne désigne aucune des deux, on retombe sur la proximité.**
Rendre « rien » était tenable tant que seules les deux zones takeout étaient
ambiguës; depuis que les meubles s'utilisent par plusieurs faces, deux postes
voisins de la cuisine sont presque toujours à portée égale, et la salle devenait
muette dès que la souris était ailleurs. La manette conserve le choix par
proximité. Toute action continue (four, préparation et service takeout) doit en
plus vérifier `Player.current_target`, sinon deux tâches proches pourraient
progresser pendant le même appui.

**Le clic gauche est aussi lié à `action`.** Quand il tombe sur un `BaseButton`
(ou l'un de ses enfants), il appartient au bouton : `Player._input()` retient
que la demande venait d'un clic (`action_came_from_click`) et
`_action_belongs_to_gui()` l'écarte. Sans ça, cliquer *Dungeon* ouvrait le
donjon **et** actionnait le meuble situé derrière. Space et la manette ne sont
jamais concernés — ils n'ont pas de curseur.

L'étagère expose ses emplacements : `get_station_slot_marks()` publie leur
position, le meuble les dessine, et chaque assiette est **au pixel près** sur sa
case. `_restack_shelf()` recale la pile à chaque dépôt et à chaque changement
d'état, sinon une upgrade de capacité redécoupe les cases et laisse les
assiettes flotter entre deux rangs. Une étagère pleine pose au sol **à côté** du
meuble, hors de son emprise : une assiette posée là ne doit pas être comptée
comme rangée.

### Quatre états d'ouverture

La salle et le comptoir ont **chacun leur rideau**, tenus par `GameState`
(`dining_close_reason`, `takeout_close_reason`) : tout fermé, salle seule,
comptoir seul, tout ouvert. `restaurant.opening_state()` les nomme.

**Un rideau ne se relève jamais seul.** Le contexte peut le baisser — plus
d'ingrédients, aucune recette à la carte, aucune recette takeout choisie, plus
rien à vendre —, seul le gérant rouvre, à la console.
`evaluate_restaurant_closure()` ne fait donc plus que *fermer*. Avant, le
magasin se rouvrait dès qu'un œuf rentrait et le joueur ne voyait jamais
pourquoi il avait fermé.

La raison est une **clé** (`CLOSE_REASON_*`), pas une phrase : elle traverse la
sauvegarde et se traduit à l'affichage. Le bandeau du HUD dit la raison et par
où rouvrir. Le comptoir tient tant qu'il lui reste des plats à vendre : c'est la
réserve cuisinée qui le garde ouvert, pas le stock d'ingrédients.

**Rideau baissé, la rue continue de vivre** : les passants traversent toujours
le trottoir, ils renoncent simplement à entrer.

### Le plan de la salle et le mode construction

**Aucune position de meuble n'est plus une constante.** Chaque chose déplaçable
a une fiche `RestaurantElement` : identifiant, genre, encombrement, position,
rotation par quarts de tour, ancrage éventuel. `station_center()` et
`table_center()` **lisent le plan**, ils ne déduisent plus d'une grille figée —
sans quoi déplacer un four laisserait son cuisinier viser le vide. Les
constantes `STATION_ORIGIN`, `TABLE_ORIGIN`… ne servent plus qu'à donner la
position de départ (`default_station_center()`).

`apply_layout()` est le **point de passage unique** du plan vers la salle :
position du nœud, taille après rotation, masque de faces tourné, collision,
centre et tabouret d'une table. Y ajouter un genre d'élément sans y ajouter son
application, c'est un meuble qui bouge à l'écran et pas dans le jeu.

Le plan vit dans `GameState.restaurant_layout` et suit la portée `dimension` :
une salle réaménagée le reste jusqu'au monde suivant. Un plan vide veut dire
« disposition d'origine », donc une partie neuve n'a rien à stocker.

Règles de placement, toutes dans `placement_blocker()`, qui rend **une raison**
et jamais un booléen — la couche verte et le fantôme rouge disent alors la même
chose, et le joueur apprend la règle en la voyant :

- un meuble ne touche jamais un mur (`WALL_CLEARANCE`, la zone libre est déjà en
  retrait) ;
- deux emprises ne se chevauchent pas ;
- un élément **ancré** échappe à la première règle mais pas à la seconde : le
  comptoir takeout reste sur le mur nord, à gauche de l'entrée, à
  `TAKEOUT_DOOR_CLEARANCE` de la porte, et ne glisse que le long de ce mur.

`BuildMode` ne connaît que des fiches : il ignore ce qu'est un four. Clic pour
prendre, `R` pour tourner, clic pour poser, Échap ou clic droit pour reposer.
**Sortir du mode en portant un meuble le repose d'où il vient** — quitter ne doit
pas être une façon de perdre un four. La couche verte est peinte par la
validation elle-même : elle ne peut donc pas mentir sur ce qui est posable.

**Poser, retirer, reprendre.** Un exemplaire n'est jamais créé ni détruit: sa
fiche existe toujours et change d'état. `placed` dit s'il est dans la salle,
`dismissed` dit qu'il en a été **retiré à la main**. Les deux sont nécessaires:
`_sync_placement_with_limits()` pose d'office ce qu'une statistique vient
d'ouvrir — acheter une upgrade doit faire apparaître son meuble — mais ne
repose jamais ce que le joueur a enlevé, sinon le × n'aurait aucun effet
durable. Un meuble à `cost` nul fait partie de la salle et se pose seul; un
meuble qui coûte quelque chose attend dans la palette.

Un poste qui cuisine et un tabouret occupé **ne se retirent pas**
(`removal_blocker`): un plat payé disparaîtrait avec eux. **La console ne se
retire jamais**, quel que soit son état: elle est le seul chemin vers
l'interrupteur du magasin *et* vers ce mode, donc la perdre bloquerait la partie.
Elle reste déplaçable.

**Kit minimum pour ouvrir** (`ESSENTIAL_FUNCTIONS`): un four, une plonge, une
poubelle, une place. `opening_blocker()` nomme ce qui manque et l'interrupteur
refuse. Le contrôle est **à l'ouverture, pas au retrait**: on réaménage
librement, on ne peut simplement pas ouvrir un magasin incapable de servir.

**Tourner une table tourne son tabouret autour d'elle.** Trois choses doivent
suivre la rotation, pas une: la place réellement occupée (`tables[i].seats`), le
dessin du siège, et le `footprint_offset` de l'élément — sinon on poserait une
table dont le tabouret finit dans un mur.

**Le mode construction s'achète** : `stat.build_permit` (0/1), accordé par le
nœud d'upgrade `build_permit`. Sans lui la console **n'affiche même pas le
bouton** — mieux vaut ne rien offrir qu'offrir un refus.

**On n'ouvre pas le chantier boutique ouverte** (`build_mode_blocker`): les deux
rideaux doivent être baissés **et** la salle vide. Déplacer une table sous un
client attablé le laisserait assis dans le vide. « Vide » veut dire les états
`ARRIVING`, `WAITING` et `EATING` — **pas** la liste `customers` entière, qui
contient aussi les passants du trottoir. La rue vivant même rideau baissé, les
compter rendait le chantier impossible à ouvrir, jamais.

Une table qui bouge **emmene ce qui est posé dessus** (`_carry_table_contents`).
Interdire le déplacement aurait reporté sur le joueur un problème que le code
règle seul.

**`apply_layout()` applique la présence, pas seulement la position.** Un meuble
retiré perd son dessin, son interaction et sa collision; un tabouret retiré sort
de `visible_table_indices()`. Sans ça le × marquait la fiche et laissait le
meuble à l'écran.

**`_restore_player_controls()` ne rend jamais la main pendant le chantier.** La
restauration est différée d'une frame: sans cette garde, le joueur reprenait les
commandes **dans** le mode construction et son clic gauche rouvrait la console
qu'il venait de quitter — le menu revenait sans arrêt par-dessus le chantier.

**Les bonus passifs se recalculent en bloc** à chaque `apply_layout()`, sous la
source `decor:<id>`, un exemplaire posé comptant une fois. La liste des
modificateurs est la vérité, l'accumulateur n'est qu'un cache (§3): retirer un
meuble retire donc son bonus sans que personne ait à s'en souvenir.

### Le restaurant tourne tout le temps

Le restaurant ne s'arrête ni en plongée, ni dans un menu, ni dans l'arbre
d'upgrades, ni pendant une scène narrative. **Seul le menu Échap suspend le
temps**, et il suspend l'arbre entier.

Quand la salle est à l'écran elle se simule elle-même (`restaurant_scene_active`
le dit) et le relais hors scène se tait : sans ce drapeau le temps compterait
deux fois. Hors scène, `GameState` accumule des secondes puis
`settle_unattended_operations()` les convertit en travail réel : plats servis,
pièces encaissées, ingrédients déduits, réserve takeout regarnie et vendue.

Le modèle hors scène est un **débit**, pas une simulation pas à pas : sans
personnage à déplacer, seule la cadence compte, bornée par le maillon le plus
lent (cuisine, service, affluence, réserve). **Un poste sans employé ne produit
rien** — la production ne devient jamais magique. La salle exige au moins un
cuisinier *et* un serveur.

Le HUD ne porte ni liste permanente d'ingrédients, ni bloc d'assiette portée,
ni indicateur marche/course. Le stock vit dans l'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.

**La console n'a plus d'onglet Shop.** Son bandeau supérieur garde toujours
visibles l'interrupteur de la salle, celui du takeout s'il est installé, le
bouton Construction si le permis est acquis et le bouton Fermer. Les pages sont
Menu, Recettes, Brigade (après son premier déblocage), Takeout et Ingrédients.
La liste seule défile et le bouton Fermer reste dans le bandeau : aucun contrôle
ne doit dépendre du bas d'une liste pour rester accessible.

**La carte du jour se constate, elle ne se compose plus.** `is_recipe_on_menu()`
vaut exactement `can_cook()` : un plat est offert tant que la réserve permet de
le sortir, et il en disparaît dès qu'elle ne le permet plus. Cocher les recettes
une par une, dans un nombre de places, était un geste répété sans décision
derrière — le joueur cochait tout ce qu'il pouvait. **Ne pas réintroduire
`menu_enabled_recipes`, `menu_slots` ni de bascule par recette.** L'onglet Menu
liste le cuisinable du plus cher au moins cher avec les portions restantes;
l'onglet Recettes montre tout l'acquis avec ses ingrédients et le stock. **Le
takeout garde sa sélection manuelle d'un plat** : c'est un choix de gérant, pas
une conséquence du stock.

`affordable_recipes()` ne filtre que sur le **prix**, jamais sur le stock : la
réserve est jugée juste après, ce qui permet de dire « rupture » au lieu de
« trop cher » quand le buffet est vide. Confondre les deux donnait au joueur une
raison fausse et cachait la vraie.

**Un meuble porte son habillage.** `InteractiveProp` prend l'image de sa fiche
`restaurant_elements` pour son quart de tour et cède son rectangle dessiné, comme
le sol du donjon cède au fond de biome. Les bordures de face restent : elles
disent où on travaille, ce qu'une image ne garantit pas. Une table est dessinée
par la salle et non par un nœud, donc elle a son propre porteur; son tabouret
reste dessiné, aucune image ne le montre.

**Une plongée se termine sur un bilan, pas sur un toast.** `DiveResult` nomme la
raison, liste la récolte et attend un geste. À la mise hors service, l'écran vire
au rouge et la liste montre `gardé / trouvé` : le joueur voit ce que l'assurance
a sauvé et ce qui reste au fond. **L'épuisement n'interrompt jamais le geste en
cours** : `try_spend_stamina()` laisse passer le coup qui vide la jauge, puis un
sursis de `EXHAUSTION_GRACE` laisse au magnétisme le temps de ramener le butin.
Refuser ce coup faisait perdre au joueur son coup *et* sa plongée.
**Les trois issues ouvrent la même fenêtre** — assommé, épuisé, **et parti de
son plein gré**. La sortie volontaire était la seule à se contenter d'un toast,
alors que c'est la seule que le joueur choisit lui-même, donc celle où il veut
voir sa récolte. Son titre lui est propre (`title_left`) : « à bout de souffle »
serait faux pour quelqu'un qui repart en pleine forme.
Les rideaux utilisent le contrôle segmenté `OpenCloseButton` : **OPEN et CLOSE
restent tous deux visibles**, la moitié active est verte ou rouge et le contour
doré conserve un focus clavier/manette explicite.

---

## 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 34**, **sauvegarde 16**. Contenu (recueil d'auteur du
25/08/2026) : 88 matériaux, 48 plats, 48 monstres, 31 patterns d'attaque,
9 patterns de mouvement, 7 biomes, 5 dimensions, **108 salles écrites**,
16 éléments de salle, 8 employés, 3 visuels, 3 profils clients, 8 récompenses,
45 statistiques, **84 nœuds d'upgrade**, 6 thèmes d’upgrade, 8 facts,
**9 meubles de restaurant**, **315 visuels** générés par PixelLab: 96 pour les
créatures (repos + marche), 88 icônes de matériaux, 48 plats dressés, 36 faces
de meubles, 12 états de clients, 24 états de brigade et **7 fonds de biome**.
Aucune fiche de ces sept collections n'est plus sans habillage.

**Refonte visuelle du 26/08/2026** — tout sauf les meubles est repassé au modèle
Pro dans un style déjanté et trash, sur des canevas quatre fois plus grands
(personnages 160 px, matériaux 128, plats 160, fonds 688×384). Le style vient
des planches d'inspiration de l'auteur, **une par région**, et deux personnages
(`gab_le_crab`, `abominable_party_man`) sont ancrés sur une image qu'il avait
retenue hors planche. **Restent en ancien style**: `big_egg_one` (quota épuisé)
et les 36 faces de meubles. Le quota se renouvelle le 25/09/2026.

**L'arbre d'upgrades touche les 44 statistiques**, en six branches (combat,
exploration, cuisine, salle, comptoir, spécial). Deux règles de son économie :
41 nœuds coûtent en plus des **matériaux du biome correspondant**, pour qu'une
plongée paie directement une amélioration ; les 10 premiers nœuds tiennent sous
25 pièces, parce qu'un plat de palier 1 se vend 1.

**Dix-huit nœuds sont lus par leur nom** dans `smoke_test.gd` et le jeu :
`big_egg_core`, `sharp_fork`, `sturdy_shell`, `quick_feet`, `deep_breath`,
`comfy_chairs`, `second_table`, `third_table`, `second_plate`, `third_plate`,
`hot_pan`, `fancy_plating`, `boss_trophy`, `auto_stove`, `wider_menu`,
`order_board`, `build_permit`, `casier`. Le test vérifie des **valeurs exactes**
(`cook_speed` −0,30 ; `payout` ×1,68 avec `fancy_plating` + `boss_trophy`) et
achète sept d'entre eux avec 1000 pièces : ces nœuds gardent leur effet, un coût
**en pièces seules**, et `sharp_fork` reste sous 5 pièces (il s'achète sur la
recette de la toute première vente).

**Temps de cuisson courts, assumés** : 2 à 5,5 s pour un plat mono selon son
palier, environ +1,5 s par ingrédient supplémentaire, 12 s au maximum pour
l'omelette finale. Les upgrades `cook_speed` descendent encore de là. Rallonger
ces durées se remarque immédiatement en jeu.

**Une recette se débloque par la découverte de ses matériaux** :
`conditions: ingredients.<id>.total_collected >= quantité`. Les deux plats
mono-ingrédient du premier palier (`squirrel_skewer`, `turkey_panic_roast`) en
sont exempts, sinon une partie neuve n'aurait aucune carte et le magasin ne
pourrait pas ouvrir.

**Schéma 29 — meubles du restaurant.** Une seule collection,
`restaurant_elements` : nom, taille, `kind` (le comportement : `stove`,
`dishwasher`, `trash`, `table`, `console`, `takeout`, `shelf`, `board`,
`spices`, `employee_locker`, `none`), `visuals` **par quart de tour** (`front`/`right`/`back`/
`left`, avec la chaîne de repli habituelle jusqu'à la forme procédurale),
`usable_faces`, `anchor`, `cost`, `modifiers` passifs.

**Une collection `station_functions` a existé et a été fondue ici (schéma 29).**
Elle était pensée clonable : un « four rapide » aurait été un four à
coefficients (`speed`, `capacity`, `quality`, `reach`). Les brancher demandait
quatre points d'insertion asymétriques, une couche locale hors des quatre
couches de statistiques, et de démêler au passage la dualité four/épicerie —
trop cher pour le gain. Sans coefficients, deux fonctions de même `kind`
étaient identiques : la collection ne faisait que renommer un mot que le code
connaît déjà. **Ne pas la réintroduire sans brancher les coefficients d'abord.**

**Schéma 33 — vagues de renfort et feuilles directionnelles.** Une salle accepte
`reinforcement_waves: 0`, qui veut dire **aucun renfort** — et non « demande
au-dessus », puisque aucune statistique ne surplombe ce champ; `DiveMap` faisait
auparavant `maxi(1, …)` et ramenait le zéro à une vague, ce qui rendait la
salle sans renfort impossible à écrire. `reinforcement_waves_max` au-dessus du
minimum fait **tirer** le nombre de vagues à l'entrée de la salle; à zéro, le
nombre déclaré sort tel quel. Côté visuels, `rows_are_directions` déclare une
feuille dont les rangées sont des orientations (§6).

**Schéma 34 — forme des salles.** Une salle déclare `scroll_axis`
(`none`, `horizontal`, `vertical`) et `scroll_extent` (1 à 2,5). `none` est
l'arène 16:9 d'origine, au pixel près; les deux autres l'étirent sur **un seul
axe** — les deux à la fois redonneraient une carte, et la caméra n'aurait plus
de cadrage sûr. L'origine ne bouge jamais : l'entrée, la sortie de secours et
les murs nord et ouest gardent leur place, seule la face opposée s'éloigne et la
porte la suit. La caméra ne se déplace **que** sur l'axe déclaré, bornée au
monde dessiné. **Une salle de fin de dimension ne défile pas** : un boss qui
sort du cadre pendant qu'il poursuit le joueur rend le combat illisible.
La grille d'apparition s'étire avec l'arène, sinon les monstres resteraient
entassés dans la première moitié de la salle.

**Schéma 32 — dépendances de pose.** `placement_requires` est une liste de
sources de meubles qui doivent déjà être sur le plancher. Le mode construction
refuse la pose tant qu'il en manque une et refuse le retrait du dernier
prérequis tant qu'un dépendant reste posé. Le `employee_locker` exige
`basic_shelf` et `basic_order_board`; sans casier posé, aucune embauche ni mise
au travail n'est possible. Retirer le casier conserve les contrats mais place
tous les employés actifs en stand-by.

Le casier suit `max_count_variable: employee_locker_count` : cette statistique
vaut `0` avant son upgrade et `1` après. Le nœud `casier` ajoute donc un
modificateur `flat +1` à la statistique, exactement comme une upgrade de table;
il n'écrit aucun effet ponctuel dans le meuble.

**Sauvegarde 15 — contrats et travail séparés.** `hired_employees` contient les
contrats acquis; `working_employees` est son sous-ensemble actif et seul ce
dernier occupe les slots, apparaît dans la salle et produit des modificateurs.
`employee_standby_reasons` mémorise `new_hire`, `manual`, `limit` ou `locker`.
L'onglet Brigade se révèle au premier déblocage de la fonction
`employee_locker`; `meta.brigade_unlocked` le garde ensuite entre les dimensions.

**Schéma 30 — prix d'un nœud d'upgrade.** `cost` + `cost_resource` sont remplacés
par une liste `costs: [{resource, amount}]`. `resource` vaut `coins` ou un
identifiant d'ingrédient : les deux sont des **quantités possédées à déduire**,
et le refus d'achat nomme précisément ce qui manque. Liste vide = gratuit.

`cost_resource` était du **texte libre que le jeu ignorait** : `purchase_upgrade`
ne dépensait que des pièces quoi qu'on y écrive. Encore un champ d'auteur que
personne ne lisait.

**Pièces et ingrédients restent deux stockages.** Ils ont la même forme — un
entier possédé — mais deux rôles : monnaie d'échange contre entrée de
production. Les fondre rendrait chaque ingrédient utilisable comme monnaie et
chaque monnaie utilisable dans une recette, et il faudrait ensuite écrire les
règles qui interdisent les combinaisons absurdes. L'unification se fait **au
point de dépense**, pas dans le stockage. Si un jour les pièces deviennent un
ingrédient, `resource` devient « toujours un ingrédient » et le format survit.

**Schéma 31 — conditions des recettes et effets de matériaux.** Chaque recette
porte `conditions`, une liste adressée combinée par ET. Elle décide si le plat
peut être placé à la carte, choisi au takeout ou cuisiné. Pour « avoir acquis X
matériaux », viser `ingredients.<id>.total_collected >= X`; pour exiger le stock
actuel, viser `ingredients.<id>.owned >= X`; une upgrade se teste par
`upgrade_nodes.<id>.purchased truthy`. Une condition de recette ne peut pas lire
`recipes.<id>.can_cook`, car ce champ réévaluerait la condition récursivement.

**L'éditeur d'upgrade n'a qu'une seule liste d'effets.** Une cible `stat.*`
donne une contribution **permanente** avec sa couche, retirée d'elle-même si
l'upgrade est perdue; une cellule stockée (fait, compteur) reçoit une écriture
**ponctuelle** à l'achat, jamais reprise. Changer l'espace d'une ligne la
déplace entre `modifiers` et `on_purchase`. **Le stockage reste séparé** : les
mélanger casserait la formule des quatre couches. Seule l'édition est unifiée.

**Le déblocage binaire passe par `conditions`, jamais par un champ d'upgrade dédié.**
Un second mécanisme en parallèle créerait deux sources de vérité pour « est-ce
disponible ». `conditions` lit n'importe quelle adresse, donc **les deux formes
existent déjà, sans une ligne de code** :

| forme | à écrire | quand |
|---|---|---|
| directe | `conditions: [{path: "upgrade_nodes.<id>.purchased", operator: "truthy"}]` | **par défaut** — un seul chemin de déblocage |
| par un fait | l'upgrade pose `{op: "set", path: "fact.<x>", value: true}` dans son crochet `on_purchase`, et l'élément lit `fact.<x>` | quand **plusieurs chemins** débloquent la même chose (une upgrade **ou** un boss vaincu **ou** une quête) |

`purchased` est un champ calculé des `upgrade_nodes`; `fact.*` est inscriptible
et `Registry.run_hook("upgrade_nodes", id, "on_purchase")` est bien joué à
l'achat. La forme directe n'a donc **rien à poser nulle part**; la forme par un
fait ne se paie que quand le point de rendez-vous sert vraiment.

La collection `restaurant_elements` alimente seule l'onglet **Meubles** de
l'outil. Chaque meuble porte directement son `kind`; il n'existe plus de
sous-onglet ni de collection séparée pour les fonctions.

**La limite par meuble est variabilisée** : `max_count_variable` nomme une
statistique qui remplace `max_count`. `stove_count` limite ainsi le four de
base, comme `table_count` limite les tabourets. Vide veut dire « la limite
écrite suffit »; une valeur résolue à `0` veut dire qu'aucun exemplaire n'est
encore débloqué. Pour un meuble quantifiable, une upgrade modifie cette
statistique — elle ne vise jamais l'espace `restaurant_elements`, qui est en
lecture seule.

### Contrat obligatoire — nouvelle fonction portée par un meuble

Quand une demande dit qu’un **nouveau meuble débloque une nouvelle fonction**,
ne pas rechercher ni inventer un autre mécanisme : lire et appliquer
[`docs/contrat-deblocage-meubles.md`](docs/contrat-deblocage-meubles.md). La
chaîne canonique est `upgrade -> modificateur flat/add -> statistique *_count ->
max_count_variable -> restaurant_element_open_count()`. Les prérequis de pose,
la présence physique du meuble et le souvenir méta d’un onglet sont trois
responsabilités séparées, également détaillées dans ce contrat. La publication
PHP, le chargement Godot et `restaurant_element_test.js` en imposent les
invariants.

**Schéma 27 — attaques de créature.** Un monstre ne porte plus `attack_pattern`,
`damage` ni `damage_variance`. Il porte une **liste** `attacks`
(`{pattern, damage_min, damage_max, cooldown, weight}`) plus le bloc de coup
direct (`direct_hit`, `direct_damage_min/max`, `direct_hit_cooldown`). Un
pattern ne porte plus de `cooldown` : la cadence appartient à l'attaque qui
l'emploie, pas à la géométrie qu'elle réutilise. `migrate_schema_27.php` fait la
conversion; il **doit** passer par `validate_and_normalize()` et
`write_json_file()`, jamais par un `json_encode` direct (§8, cartes vides).
Les salles sont écrites par l'utilisateur.

**Butin** : `drops` est une liste ordonnée de `{ingredient_id, chance}`. Chaque
ligne fait un jet indépendant de 0 à 100 %, y compris lorsque plusieurs lignes
nomment le même ingrédient. Un jet réussi produit exactement une unité; `100`
est garanti. `stat.drop_count_multiplier` multiplie le nombre de jets de chaque
ligne : sa partie entière est garantie et sa fraction est la chance d'un jet
supplémentaire (`1,25` = 1 jet + 25 % d'un second). Les quantités de recettes
conservent leur sémantique distincte.

**Magnétisme du butin** : `stat.drop_magnet_range` est le rayon en pixels (`0`
désactive) et `stat.drop_magnet_speed` la vitesse en pixels/seconde. Après
l'animation d'apparition, chaque `IngredientPickup` libre situé dans ce rayon
avance vers le héros à cette vitesse; la collecte reste déclenchée par le
contact normal. Sa collision suit le sol tandis que le visuel répète une
parabole configurée par `Magnet Bounce / magnet_hop_height` et
`magnet_hop_distance` dans la scène du ramassable.

**Les quatre colonnes sont écrites (25/08/2026)** : 14, 18, 22 et 46 cases,
plus 5 cases pour `fork_trial`, d'après les routes du recueil d'auteur. Chaque
colonne déclare exactement une case `ends_dimension`. Le filet `_generated_rooms`
ne s'applique donc plus : **une case ajoutée sans revoir la fin de colonne rend
la plongée interminable**. La fourchette maîtresse s'obtient de deux façons — la
case événement de la dimension 4 et l'épreuve hors cycle — qui posent le même
`fact.master_fork_owned`; la case finale du World Egg Center le lit et convoque
l'épreuve par `on_blocked`.

**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 scène couvre désormais les ventes **et** la production :
  comptoir regarni, salle servie, ingrédients déduits. Ce qu'elle ne rejoue pas,
  ce sont les **positions** des employés pendant l'absence — elles reprennent là
  où elles avaient été capturées au départ.
- **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~~ — **construit le 25/08/2026**, et
  posé sur les 48 créatures, les 3 clients, les 8 employés **et le héros**. Le
  champ `rows_are_directions` dit qu'une feuille range ses **rangées comme des
  orientations**, du sud au nord en passant par l'est: `VisualHolder` choisit la
  rangée, et son miroir horizontal fournit les trois vues ouest. Cinq rangées
  couvrent donc les huit points cardinaux, ce qui est exactement ce que rend
  PixelLab. Sans le drapeau, la feuille se joue comme une animation unique —
  c'est resté le comportement par défaut.

---

## 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.
- **Un contrat n'est pas un poste actif.** L'engagement commence en stand-by;
  seul « Faire travailler » consomme un slot. Une perte de slot ou de casier
  replace le travailleur en stand-by sans annuler le contrat.
- 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. Une carte
  vide normalisée devient un `stdClass` : employer `collection_count()` dans les
  comptes-rendus PHP, jamais `count()` directement.
- **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 « Abandonner et recharger » avant de publier** dès que les données
  ont été touchées ailleurs (cette commande repart du maître publié et remplace
  aussi le brouillon serveur).
- **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

- **Ouvrir le projet avec un autre exécutable Godot le réécrit.** Un éditeur 4.5
  lancé sur ce projet a remis `config/features` en `"4.5"` et **le clavier a
  cessé de répondre**, dans les deux projets, alors que la manette continuait —
  elle est interrogée globalement, le clavier passe par les événements. Devant un
  clavier muet, vérifier d'abord `config/features` : s'il n'annonce pas 4.7, le
  projet a été ouvert par le mauvais binaire.
- **Écrire `project.godot` en binaire, jamais en mode texte.** Une réécriture
  Python en mode texte l'a converti en CRLF; les valeurs `Object(...)` de la
  section `[input]` s'étalent sur plusieurs lignes et leur analyse casse. Les
  **actions disparaissent** : `Input.is_physical_key_pressed()` reste vrai et
  `Input.get_vector()` rend zéro. Symptôme identique au précédent, cause
  opposée — d'où l'intérêt d'afficher les deux à l'écran.
- **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. **Le 23/08/2026 le retour a emporté six
  fichiers d'un coup et remontait très loin** : `config.php` était revenu en
  schéma 19, `data_catalog.gd` avait retrouvé `appearance`/`state_styles`. Donc:
  devant une erreur inexplicable — surtout un `Cannot find member` sur une
  constante qui existe — **balayer tout l'arbre contre le dernier `_backups/`
  avant de chercher la cause dans son propre code**:
  `for f in $(cd BACKUP && find . -type f); do ... comparer les tailles ...`
  Un fichier plus court que sa sauvegarde sans raison connue est un retour en
  arrière, pas un bug.
- **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 sept 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). **Un témoin qui peut
   mourir en cours de série est instable par construction**: le héros frappe à
   1000 et le témoin était planté à 1000 points de vie, donc la variance de
   dégâts décidait au tirage s'il tenait jusqu'au contrôle suivant. Sa vie se
   dérive des dégâts réels, et un coup fatal se mesure sur la vie de la cible
   (`take_damage(cible.health + 1)`), jamais sur un grand nombre écrit à la main.
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 `await` dans une fonction de contrôle en fait une coroutine**, et
   l'appelant reprend la main au premier `await`: tout ce qui suit est abandonné
   en silence, et le test annonce « OK ». Une fonction de contrôle reste
   synchrone, ou son appelant l'attend explicitement. Corollaire: un repli qui
   rend la valeur attendue (`return focus` quand le calcul est impossible) fait
   comparer une valeur à elle-même — éprouver d'abord la précondition.
7. **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**, la vérifier une fois en
  cassant le code (§8, règle 4), puis valider **au niveau du changement** (§2)
  — pas toute la batterie à chaque modification.
- **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 |
| Forme des salles | un seul axe de défilement, 2,5 fois l'arène au plus; jamais sur une salle de boss |
| 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; contrats séparés du travail actif, casier absent = stand-by; onglet Brigade déverrouillé en portée `meta` |
| Comptoir takeout | recette choisie **directement** dans l'onglet Takeout de la console; zone préparation jusqu'au plafond, cuisiniers sous X jusqu'à Y; zone 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); prix de vente = `stat.takeout_price_ratio` (0,75) du prix de table |
| Ouverture | deux rideaux indépendants (salle, comptoir) = quatre états; aucune réouverture automatique, jamais |
| Carte du jour | dérivée de `can_cook`, jamais choisie; le takeout garde sa sélection manuelle |
| Fin de plongée | fenêtre de bilan pour les **trois** issues (assommé, épuisé, départ volontaire); l'épuisement laisse passer le dernier geste |
| Temps | le restaurant tourne toujours; seul le menu Échap suspend l'arbre |
| Interaction | faces utilisables + bordure intérieure à 15 %; plus aucun tapis au sol |
| Plan de la salle | fiches `RestaurantElement`, déplaçables en mode construction, persistées en portée `dimension` |
| Meubles | une collection `restaurant_elements`; chaque objet posé porte directement son `kind` et ses `placement_requires` |
| Déblocage quantifié d'un meuble | `max_count_variable`; valeur `0` = aucun exemplaire, l'upgrade ajoute à la statistique |
| Limite par meuble | `max_count` reste le plafond absolu |
| Placement | jamais collé à un mur, jamais superposé; le comptoir takeout est ancré au mur nord à gauche de l'entrée |
| Sauvegardes | trois slots sous `user://saves`, écriture temporaire validée + `.bak`; options globales séparées dans `user://settings.json`; l'ancien fichier migre vers le slot 1 sans suppression |
| Visuels | collection `visuals` partagée, animations en feuille + grille, décor en image de fond |
| Scènes narratives | acteurs présentationnels ancrés à la scène ou entre eux; événements séquentiels par ID, choix/branchements, bulles, déplacements, VFX et écritures limitées aux adresses inscriptibles |
| 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** |
| Branche de travail | `big-egg-3d/` depuis le 27/08/2026; `big-egg/` est une référence en lecture seule |
| Rendu 3D | la logique 2D reste l'autorité et pilote des avatars; le pont ne décide de rien. 1 px = 1 unité. Deux champs seulement (`shape`, `tall`) dans `shapes_3d.json`. Neuf gardes `if Bridge3D.active: return` en tête des `_draw()` des corps du monde |

---

## 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 |
| `big-egg-3d/LISEZMOI-3D.md` | **la branche de travail**: le jeu entier, rendu en 3D par un pont |
