# 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.

**Format : une ligne par règle.** Le tableau « Décisions arrêtées » est le
modèle — une décision y tient sur une ligne là où un paragraphe la raconte
ailleurs. Prendre un paragraphe seulement quand la règle est inintelligible sans
son contexte. **Le pourquoi détaillé va dans `docs/journal-conception.md`**, qui
existe pour ça et qu'on relit avant de rouvrir une décision. Recensement du
27/08/2026 : 1184 → 1000 lignes, sans qu'aucune règle soit perdue.

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

**À l'arrêt jusqu'au 25/09/2026** (quota épuisé, 315 visuels produits). Les
douze leçons du pipeline — composition des bandes, mesure de l'échelle sur la
case sud, fusion des groupes d'animation homonymes, plafond de travaux
simultanés — sont dans [`docs/pipeline-pixellab.md`](docs/pipeline-pixellab.md).
**Les relire avant de relancer un lot.**

---

## 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, takeout, et faces d'interaction

Les quatre `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écident ensuite de la
vitesse ou de la qualité; **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 le réinitialise
jamais. Sa barre affiche l'avancement du service; la patience reste invisible.

Le comptoir a 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. Ce 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: `move_speed` (400 px/s) et
`move_stamina_per_second`. Ne pas réintroduire de bascule marche/course.

**Les tapis d'interaction n'existent plus.** Chaque `InteractiveProp` actif
déclare un masque `usable_faces` et dessine sur chaque face une **bordure
intérieure** dont la profondeur vaut `face_band_ratio` (0,15) de la dimension
**perpendiculaire**. 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. Masque vide = ancien comportement, un simple rayon
autour de `interaction_offset`: c'est ce qui laisse le donjon intact.

`interaction_offset` ne décide plus de la portée; il déclare **par quel côté on
aborde le meuble par défaut** (`default_stand_position()`).
`get_interaction_position()` suit la face la plus proche, écartée de
`face_stand_gap` (26 px). Le HUD reprend `Player.current_target`: **ne pas
réintroduire une seconde recherche de proximité dans le HUD.** Seul le meuble
reçoit un contour, collé à son bord (groupe `Target Outline`). Le prompt est une
ligne discrète: un verbe court quand `can_interact()`, puis il disparaît pendant
que Action est maintenue. Une raison précise reste en rouge quand un poste fixe
est temporairement indisponible; Action l'appelle tout de même pour que son refus
passe par le toast (`allows_unavailable_prompt()`).

**La portée physique est toujours le premier filtre.** Si deux cibles 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, ou le tapis invisible juste devant (`face_apron_rect`).
**Quand le curseur ne désigne aucune des deux, on retombe sur la proximité** —
depuis que les meubles s'utilisent par plusieurs faces, deux postes voisins sont
presque toujours à portée égale, et la salle devenait muette dès que la souris
était ailleurs. La manette garde 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 progresseraient sur le même appui.

**Le clic gauche est aussi lié à `action`.** Quand il tombe sur un `BaseButton`
(ou un enfant), il appartient au bouton: `Player._input()` retient
`action_came_from_click` et `_action_belongs_to_gui()` l'écarte. Sans ça, cliquer
*Dungeon* ouvrait le donjon **et** actionnait le meuble derrière. Space et la
manette ne sont jamais concernés.

L'étagère expose ses emplacements (`get_station_slot_marks()`) 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é laisse les assiettes
flotter entre deux rangs. Une étagère pleine pose au sol **à côté** du meuble:
une assiette posée là n'est pas 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 une constante.** Chaque chose déplaçable a une
fiche `RestaurantElement`: identifiant, genre, encombrement, position, rotation
par quarts de tour, ancrage. `station_center()` et `table_center()` **lisent le
plan** — sinon déplacer un four laisserait son cuisinier viser le vide. Les
constantes `STATION_ORIGIN`, `TABLE_ORIGIN`… ne donnent plus que la position de
départ.

**`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. Il applique aussi la **présence**: un meuble
retiré perd son dessin, son interaction et sa collision. 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`, portée `dimension`. Plan vide =
disposition d'origine, donc une partie neuve n'a rien à stocker.

**`placement_blocker()` rend une raison, 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. Trois règles: jamais contre un mur (`WALL_CLEARANCE`); jamais deux
emprises superposées; un élément **ancré** échappe à la première mais pas à la
seconde (le comptoir takeout reste sur le mur nord, à gauche de l'entrée, à
`TAKEOUT_DOOR_CLEARANCE` de la porte).

**`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.

**Poser, retirer, reprendre.** Un exemplaire n'est jamais créé ni détruit: sa
fiche change d'état. `placed` dit s'il est dans la salle, `dismissed` 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, mais ne repose **jamais** ce que le joueur a enlevé. Un meuble à `cost`
nul se pose seul; un meuble payant attend dans la palette.

**Ce qui ne se retire pas** (`removal_blocker`): un poste qui cuisine et un
tabouret occupé — un plat payé disparaîtrait avec eux. **La console jamais**,
quel que soit son état: elle est le seul chemin vers l'interrupteur *et* vers ce
mode. 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. Le contrôle est **à
l'ouverture, pas au retrait**: on réaménage librement.

**Tourner une table tourne son tabouret autour d'elle**, et trois choses doivent
suivre: la place occupée (`tables[i].seats`), le dessin du siège, et le
`footprint_offset` — sinon on pose une table dont le tabouret finit dans un mur.

**Le mode s'achète**: `stat.build_permit`, accordé par le nœud `build_permit`.
Sans lui la console **n'affiche 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 baissés **et** la salle vide. « Vide » veut dire les états `ARRIVING`,
`WAITING` et `EATING` — **pas** la liste `customers`, qui contient les passants du
trottoir; les compter rendait le chantier impossible à ouvrir, jamais.

Une table qui bouge **emmène ce qui est posé dessus** (`_carry_table_contents`).

**`_restore_player_controls()` ne rend jamais la main pendant le chantier**, et sa
restauration est différée d'une frame: sans cette garde, le clic gauche du joueur
rouvrait la console qu'il venait de quitter.

**Les bonus passifs se recalculent en bloc** à chaque `apply_layout()`, sous la
source `decor:<id>`, un exemplaire posé comptant une fois. Retirer un meuble
retire donc son bonus sans que personne ait à s'en souvenir.
### Le restaurant tourne tout le temps

Il 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`)
et le relais hors scène se tait: sans ce drapeau le temps compterait deux fois.
Hors scène, `settle_unattended_operations()` convertit les secondes accumulées en
travail réel. Le modèle est un **débit**, pas une simulation pas à pas: seule la
cadence compte, bornée par le maillon le plus lent. **Un poste sans employé ne
produit rien** — la salle exige au moins un cuisinier *et* un serveur.

Le HUD ne porte ni liste d'ingrédients, ni bloc d'assiette portée, ni indicateur
marche/course. Le stock vit dans l'onglet Ingrédients. Le plat porté est nommé
au-dessus du joueur. Objectif, toast et fermeture restent de petites lignes dans
le coin inférieur gauche, **jamais des messages centraux**.

**La console n'a plus d'onglet Shop.** Son bandeau garde toujours visibles les
interrupteurs, le bouton Construction si le permis est acquis, et Fermer. Les
pages sont Menu, Recettes, Brigade, Takeout, Ingrédients. **Seule la liste
défile**: aucun contrôle ne doit dépendre du bas d'une liste.

**La carte du jour se constate, elle ne se compose plus.** `is_recipe_on_menu()`
vaut exactement `can_cook()`. Cocher les recettes une par une était un geste
répété sans décision derrière. **Ne pas réintroduire `menu_enabled_recipes`,
`menu_slots` ni de bascule par recette.** Le takeout garde sa sélection manuelle:
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 ». Les confondre donnait au joueur une raison fausse.

**Un meuble porte son habillage.** `InteractiveProp` prend l'image de sa fiche
pour son quart de tour et cède son rectangle dessiné. 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, donc elle a son propre porteur.

**Une plongée se termine sur un bilan, pas sur un toast.** `DiveResult` nomme la
raison, liste la récolte et attend un geste. **Les trois issues ouvrent la même
fenêtre** — assommé (écran rouge, liste `gardé / trouvé`), épuisé, et parti de
son plein gré (titre propre `title_left`). **L'épuisement n'interrompt jamais le
geste en cours**: `try_spend_stamina()` laisse passer le coup qui vide la jauge,
puis `EXHAUSTION_GRACE` laisse au magnétisme le temps de ramener le butin.

Les rideaux utilisent `OpenCloseButton`: **OPEN et CLOSE restent tous deux
visibles**, la moitié active est verte ou rouge, le contour doré marque le focus.

## 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**.

**Restent en ancien style visuel**: `big_egg_one` et les 36 faces de meubles.
Tout le reste est passé au modèle Pro le 26/08/2026. Quota au 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 — `restaurant_elements`.** Nom, taille, `kind` (`stove`,
`dishwasher`, `trash`, `table`, `console`, `takeout`, `shelf`, `board`, `spices`,
`employee_locker`, `none`), `visuals` par quart de tour, `usable_faces`, `anchor`,
`cost`, `modifiers` passifs.

**Ne pas réintroduire `station_functions`** (fondue dans les meubles au schéma 29)
sans brancher d'abord ses coefficients: sans eux, deux fonctions de même `kind`
sont identiques et la collection ne fait que renommer un mot que le code connaît.

**Schéma 33.** `reinforcement_waves: 0` veut dire **aucun renfort**, pas « demande
au-dessus »: aucune statistique ne surplombe ce champ. `reinforcement_waves_max`
au-dessus du minimum fait tirer le nombre de vagues à l'entrée.
`rows_are_directions` déclare une feuille dont les rangées sont des orientations.

**Schéma 34 — forme des salles.** `scroll_axis` (`none`/`horizontal`/`vertical`)
et `scroll_extent` (1 à **2,0**). **Un seul axe** — les deux redonneraient une carte.
L'origine ne bouge jamais: entrée, sortie de secours et murs nord/ouest gardent
leur place, seule la face opposée s'éloigne et la porte la suit. **Une salle de fin
de dimension ne défile pas.** La grille d'apparition s'étire avec l'arène.

**Schéma 32 — `placement_requires`.** Liste de sources de meubles qui doivent déjà
être posées. Le mode construction refuse la pose tant qu'il en manque une, et le
retrait du dernier prérequis tant qu'un dépendant reste posé. `employee_locker`
exige `basic_shelf` et `basic_order_board`; sans casier, aucune embauche. Le
retirer conserve les contrats et place les employés actifs en stand-by.

**Sauvegarde 15.** `hired_employees` = les contrats; `working_employees` = son
sous-ensemble actif, seul à occuper un slot, à apparaître et à produire des
modificateurs. `employee_standby_reasons` mémorise `new_hire`, `manual`, `limit`
ou `locker`. `meta.brigade_unlocked` garde l'onglet Brigade entre les dimensions.

**Schéma 30 — prix d'un nœud.** Une liste `costs: [{resource, amount}]`;
`resource` vaut `coins` ou un identifiant d'ingrédient, tous deux des quantités
possédées à déduire. Liste vide = gratuit. L'ancien `cost_resource` était **du
texte que le jeu ignorait** — encore un champ d'auteur que personne ne lisait.

**Pièces et ingrédients restent deux stockages.** Même forme, deux rôles: monnaie
contre entrée de production. Les fondre obligerait ensuite à écrire les règles qui
interdisent les combinaisons absurdes. **L'unification se fait au point de dépense.**

**Schéma 31 — `conditions` de recette.** Liste adressée combinée par ET; décide si
le plat peut être à la carte, choisi au takeout ou cuisiné. « Avoir acquis X » vise
`ingredients.<id>.total_collected`; le stock actuel vise `.owned`; une upgrade se
teste par `upgrade_nodes.<id>.purchased truthy`. **Une condition ne peut pas lire
`recipes.<id>.can_cook`**: elle se réévaluerait récursivement.

**L'éditeur d'upgrade n'a qu'une seule liste d'effets, mais deux stockages.** Une
cible `stat.*` donne une contribution **permanente** (`modifiers`); une cellule
stockée reçoit une écriture **ponctuelle** à l'achat (`on_purchase`). **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édié.** Deux
sources de vérité pour « est-ce disponible » divergeraient. Par défaut, viser
directement `upgrade_nodes.<id>.purchased truthy`; quand **plusieurs chemins**
débloquent la même chose, l'upgrade pose un `fact.<x>` dans `on_purchase` et
l'élément lit ce fait. Les deux formes existent déjà, sans une ligne de code.

La collection `restaurant_elements` alimente seule l'onglet **Meubles** de l'outil.

**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
rien inventer: lire et appliquer
[`docs/contrat-deblocage-meubles.md`](docs/contrat-deblocage-meubles.md). Chaîne
canonique: `upgrade -> modificateur flat/add -> statistique *_count ->
max_count_variable -> restaurant_element_open_count()`. Prérequis de pose,
présence physique et souvenir méta d'un onglet sont **trois responsabilités
séparées**. Les invariants sont imposés par la publication PHP, le chargement
Godot et `restaurant_element_test.js`.

**Schéma 27 — attaques de créature.** Un monstre porte une **liste** `attacks`
(`{pattern, damage_min, damage_max, cooldown, weight}`) plus son bloc de coup
direct. **Un pattern ne porte plus de `cooldown`**: la cadence appartient à
l'attaque, pas à la géométrie qu'elle réutilise.

**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 fois** l'arène au plus (abaissé de 2,5 le 27/08/2026, 26 salles écrêtées); 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 |
| `docs/pipeline-pixellab.md` | génération des visuels — **à relire avant de relancer un lot** |
| `big-egg-3d/LISEZMOI-3D.md` | **la branche de travail**: le jeu entier, rendu en 3D par un pont |
