# BIG EGG — directives de projet

Fichier de référence chargé à chaque session. **À maintenir à jour** : toute
nouvelle règle, tout piège découvert et tout changement de version se consigne
ici. Il sert aussi de point de reprise en cas de perte de contexte ou de reprise
par un autre agent.

Langue de travail : **français** (l'utilisateur écrit en français). Les
identifiants, noms de fonctions et commentaires de code restent en anglais,
conformément au code existant.

---

## 1. Environnement

| | |
|---|---|
| Racine de travail | `E:\GDRIVE\SERVEUR ET PRODUCTION WEB\2025\REBIRTH\BIG EGG` |
| Projet Godot | `big-egg/` |
| Outil de données | `big-egg/tools/game-data-manager/` |
| Godot | `C:\Users\ELR17\Downloads\Godot_v4.7.1-stable_win64.exe\Godot_v4.7.1-stable_win64_console.exe` |
| PHP | `C:\wamp64\bin\php\php7.4.33\php.exe` |
| Node | `C:\Program Files\nodejs\node.exe` |

**Pas de dépôt Git.** Le filet de sécurité est une copie datée dans `_backups/`
avant chaque chantier notable :

```bash
STAMP=$(date +%Y%m%d-%H%M) && DEST="_backups/big-egg_<motif>_$STAMP" \
  && mkdir -p "$DEST" && tar -cf - --exclude=.godot -C . big-egg | tar -xf - -C "$DEST"
```

Ne jamais tuer un éditeur Godot déjà ouvert. Préserver les fichiers et le travail
existants.

---

## 2. Validation — à lancer après tout changement

```bash
# 1. Parse de tous les scripts et scènes
godot --headless --editor --path 'big-egg' --quit

# 2. Smoke test complet (le principal filet)
godot --headless --path 'big-egg' 'res://scenes/main/smoke_test.tscn'

# 3. Retour depuis l'arbre d'upgrades
godot --headless --path 'big-egg' 'res://scenes/main/upgrade_return_smoke.tscn' --quit-after 3

# 4. Outil
node --check big-egg/tools/game-data-manager/assets/app.js
node big-egg/tools/game-data-manager/tests/address_test.js
node big-egg/tools/game-data-manager/tests/localization_test.js
node big-egg/tools/game-data-manager/tests/render_test.js
node big-egg/tools/game-data-manager/tests/dive_editor_test.js
node big-egg/tools/game-data-manager/tests/room_editor_test.js
# Après avoir ajouté une clé à Loc.UI_FALLBACKS:
node big-egg/tools/game-data-manager/tests/seed_ui_texts.js
php -l config.php ; php -l api.php ; php -l index.php
```

### Pièges d'exécution — coûteux si oubliés

- **Ne pas rediriger une longue commande Godot dans `grep`.** Le pipe bufferise
  et la commande semble bloquée pendant des minutes. Écrire dans un fichier puis
  filtrer : `godot ... > /tmp/s.log 2>&1; grep -E "SMOKE TEST" /tmp/s.log`.
- **Un nouveau script avec `class_name` doit passer par l'éditeur avant tout
  lancement headless**, sinon `Identifier "X" not declared`. Lancer d'abord
  `--headless --editor --quit` pour enregistrer la classe globale.
- Le smoke test tourne en ~6 s. Au-delà, c'est un blocage réel.
- Certains contrôles de combat sont sensibles au timing physique. Un échec isolé
  sur le dash ou la détection ennemie se revérifie par une seconde exécution
  avant d'être traité comme une régression.

---

## 3. Architecture — l'essentiel

### 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`).

```
fact.boss_defeated
stat.attack_damage
enemies.fungling.total_killed
recipes.fried_egg.can_cook
```

**Lecture uniforme, écriture étroite** : tout se lit pareil, mais seules les
*cellules stockées* sont inscriptibles. Une valeur calculée a un résolveur, pas
de setter. Trois natures de champs :

| 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 les 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
```

- `flat` : +10 d'une arme
- `scaling` : pourcentages cumulatifs de progression (+0,25 par upgrade)
- `global` : coefficient général, **séparé 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` : vrai multiplicatif isolé, rare

**La liste de modificateurs est la vérité, l'accumulateur est un cache.** Ne
jamais muter une valeur finale : une valeur mutée ne sait pas se dé-muter, et
un effet temporaire ne survivrait pas à une sauvegarde.

Sources nommées `<type>:<id>` (`upgrade:`, `equipment:`, `consumable:`,
`debuff:`, `set:`, `run:`). Retrait par source : `remove_modifiers_by_source()`.

**Ordre d'application 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 trois choses :
modificateurs de statistiques, actions périodiques (dégâts), 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` → l'expiration par pile
  est gratuite. `stack` = piles indépendantes, `stack_refresh` = expiration
  commune. Défaut de clé : l'identifiant de l'effet (seul cas où `max_stacks`
  garantit un vrai plafond).
- **Les instances ne sont pas adressables** : trois slimes sont trois instances
  d'une définition. Seuls les agrégats remontent vers l'espace d'adressage.
- Les effets du joueur vivent dans `GameState.player_effects` et sont réhydratés
  après chaque changement de scène, comme les assiettes.

### Effets déclaratifs et crochets

Crochets par collection : `on_kill`, `on_spawn`, `on_damage_player`,
`on_cooked`, `on_served`, `on_collected`, `on_purchase`, `on_left_unserved`.

Un effet est un enregistrement **plat** : `{op, path, value}` avec
`op ∈ {set, add, multiply, min, max, toggle}`. **Aucune expression, aucune
condition imbriquée** — dès qu'on en ajoute, ça devient un langage de script, et
un mauvais. Ce qui ne rentre pas reste en GDScript.

### Réactivité

Recalcul global via `state_changed`. **Ne pas construire d'invalidation fine par
chemin** : à cette échelle (15 nœuds, 5 monstres) elle n'apporte rien et ajoute
des abonnements fantômes. `Registry.write()` étant le point de passage unique,
elle reste greffable plus tard.

### Contrôles de cohérence

À 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. Les invariants
tiennent par construction.

---

## 4. Textes multilingues — **directive active**

> **Tout nouveau texte affiché passe par un identifiant.** Jamais de littéral
> dans le code.

- Texte d'interface : ajouter la clé dans `Loc.UI_FALLBACKS`
  (`scripts/core/loc.gd`) et appeler `Loc.ui("ui.domaine.nom")`.
- Nom d'entité : passer par `DataCatalog.ingredient_name()`, `recipe_name()`,
  `enemy_name()`, `customer_name()`, `upgrade_name()`, `variable_name()`.
- Puis lancer le semis pour ajouter la clé aux fichiers de données (ou l'ajouter
  via l'onglet **Textes** de l'outil).

**La clé de traduction est une adresse** : `ingredients.egg.display_name`,
`stat.attack_damage.display_name`, `ui.restaurant.plates`. Le catalogue se dérive
donc des données — aucun registre manuel. Les collections traduisibles et leurs
champs sont déclarés dans `Loc.TEXT_FIELDS`, en miroir de
`localization_text_fields()` côté PHP et `LOCALIZATION_TEXT_FIELDS` côté JS.

Résolution : `langue courante → langue de repli (en) → défaut d'auteur → texte
du code → la clé`.

Le **défaut d'auteur** est le texte saisi dans les formulaires de l'outil
(français, anglais ou mélangé, peu importe). Il est resynchronisé à chaque
publication. Il n'est **pas destiné à être livré** : la version finale active
`Loc.strict_mode`, qui signale chaque texte non traduit au lieu de retomber
dessus silencieusement.

Langues actuelles : `fr`, `en`. Ajout d'une langue depuis l'onglet Textes.

**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 actuelles : **schéma de données 10**, **sauvegarde 6**.
Contenu : 5 ingrédients, 5 plats, 5 monstres, 3 profils clients, 15 nœuds
d'upgrade, 7 biomes, 5 dimensions, 8 récompenses, 6 éléments de salle,
94 cases + 2 salles alternatives, 19 statistiques, 8 facts, 218 clés de texte.

**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 chaque fois que c'est praticable.

---

## 5b. Combat — points de conception hérités

- **Couches physiques** : joueur `1`, ennemis et murs `2`, ramassables `4`.
- L'attaque du joueur fait une requête physique large puis filtre par portée et
  par produit scalaire de direction : c'est un **éventail vers l'avant**, pas un
  cercle. Les API de dégâts prennent direction et force, donc joueur et ennemis
  appliquent le même recul dégressif.
- Les ennemis testent **distance et angle** avant de détecter. Mouvements sans et
  avec détection choisis indépendamment, puis orientés par `chase`/`avoid`/`ignore`.
- La géométrie d'attaque vit dans des **patterns réutilisables**, pas dans la
  fiche du monstre : `direct_hit` résout au contact, `burst` fige origine et
  direction, `sticky` suit le héros jusqu'à l'impact, `swipe` fait tourner son
  secteur pendant `active_duration`. Chaque état a un chemin de fin explicite,
  sinon un visuel reste accroché. Recevoir un recul annule l'état et rend 65 %
  du temps de recharge.
- `ShapeFactory` mappe les formes du catalogue vers les polygones de dessin
  **et** les formes physiques : aperçu navigateur, silhouette et collision
  viennent d'un seul réglage.

---

## 5c. Structure d'une plongée — construit

```
dimension → colonne de salles écrites → salle de boss
```

**La salle est l'unité écrite.** Elle nomme son biome, choisit ses créatures,
ses récompenses et ses modificateurs. Le biome n'est plus qu'une **palette** :
habillage du décor, et table de repli quand la salle ne déclare rien.

`DiveMap` (scripts/core) répond seul à « où en est la plongée » et « qu'est-ce
qui vient ensuite ». Il ne connaît pas la scène du donjon.

| Portée | Champ | Rôle |
|---|---|---|
| `meta` | `dimension_index` | rang dans le cycle, boucle après la dernière |
| `meta` | `gate_pending` | épreuve convoquée par un verrou |
| `run` | `dimension_id` | **figée à l'entrée** de la plongée |
| `run` | `level` | rang de la case, à partir de 1 |
| `run` | `room_id` | salle réellement traversée, **figée** elle aussi |
| `run` | `special_used` | combien de fois chaque salle alternative est sortie |
| `run` | `boss_room` | la plongée est devant le boss |

**Deux choses sont figées dans `run`, pour la même raison.** La dimension :
sans ça, franchir une porte après avoir gagné la fourchette maîtresse changerait
de dimension en cours de plongée. La salle : sans ça, chaque relecture
retirerait au sort le remplacement par une salle alternative.

### La règle qui tient tout

> **Zéro ou vide sur une salle veut dire « demande au-dessus ».**

Effectif à 0 → statistique + marche du biome. Table de monstres vide → table du
biome, puis tout le bestiaire. Choix de récompense à 0 → statistique. C'est ce
qui rend une colonne générée jouable sans l'avoir réglée case par case.

### Salles alternatives

Une salle sans `dimension` n'occupe aucune case : elle survient en **remplaçant**
une case normale, selon `replaces = [{dimension, level, chance}]`.

**La désignation est positionnelle** : « la case N de telle dimension ».
Conséquence assumée : insérer ou retirer une case décale ce que les
remplacements visent. `renumberColumn()` refait la suite à chaque suppression,
et l'outil signale une cible au-delà de la colonne.

**Arbitrage** — la probabilité la plus **haute** décide s'il y a remplacement,
puis le choix se fait au poids proportionnel entre les prétendantes. La somme
n'entre jamais dans le calcul, donc **dépasser 100 % n'est pas une erreur** : il
n'y a rien à valider, seulement une information à afficher. Contrepartie :
ajouter une salle alternative n'augmente pas la fréquence des salles spéciales,
elle dilue seulement la part des autres. `max_per_run` plafonne les apparitions,
`0` valant « sans limite ».

### Le reste de la plongée

- **Biome** : habillage (trois couleurs), table de repli, marche de population,
  et `room_count` qui ne sert plus qu'à **générer** une colonne.
- **Dimension** : boss (une créature du bestiaire agrandie par trois facteurs
  déclarés), `order ≥ 1` pour le cycle, `order = 0` pour une épreuve hors cycle.
- **Verrou** : `gate_level` (rang de **case**, plus de biome), `gate_fact`,
  `gate_dimension` se déclarent **ensemble ou pas du tout**. Buter dessus
  convoque l'épreuve, qui passe devant le cycle à la plongée suivante et accorde
  le fait par son crochet `on_complete` — **aucun code dédié**.
- **Renforts** : durée > 0 → vagues régulières étalées sur cette durée ; durée
  = 0 → un bloc quand la vague d'ouverture faiblit. Une salle n'est nette que
  lorsque **les deux budgets** sont épuisés. Pool distinct de celui d'ouverture.
- **Récompenses** : tirage pondéré sans remise dans une offre (deux fois la même
  carte annulerait le choix), avec remise entre deux offres. Source `run:`.
- **Modificateurs de salle** : source unique `run:room`, posés en entrant,
  retirés en sortant. Les retirer est une opération, pas un inventaire à tenir.
- **Assurance** : tirage sur les **unités**, pas sur les types, sinon elle
  effacerait systématiquement le tas le plus gros.

### Filet de secours sans cent littéraux

`DataCatalog.ROOMS` par défaut n'est **pas écrit à la main** : `_generated_rooms`
le déduit des biomes de chaque dimension. Même chose au chargement — une donnée
sans salle retombe sur cette génération plutôt que sur une plongée vide. C'est
aussi ce que fait le bouton « générer » de la colonne dans l'outil.

Le budget de salle reste un **plafond, pas une garantie** : quand les
`spawn_max` ne peuvent pas l'absorber, une salle plus petite est valide.

### Écrit mais inerte

Les **éléments de salle** (`room_elements` : destructibles, pièges, coffres,
PNJ, magasins) et les **comportements** (`behaviours` : trésor, magasin, élite,
repos, énigme, embuscade, histoire) sont écrits, validés et publiés. **Le jeu ne
les fait pas encore apparaître ni ne les interprète.** Leur comportement est un
chantier à part.

---

---

## 6. Pièges spécifiques à ce projet

- **`action` est aussi lié au clic gauche** (`project.godot`). `Node._input()`
  tourne **avant** le routage GUI : y consommer un `InputEventMouseButton`
  empêche tout bouton de recevoir le clic. Toujours laisser passer les
  `InputEventMouseButton` dans `_input`.
- **Une garde `is_instance_valid()` dans une boucle typée ne protège pas** d'une
  instance libérée : l'assignation de `for x: Type in ...` échoue avant le corps.
  Les entités qui s'inscrivent dans une liste doivent s'en retirer elles-mêmes
  dans `_exit_tree()` (voir `Customer._exit_tree`), et non compter sur l'appelant.
- **PHP encode un tableau associatif vide en `[]`, pas `{}`.** Godot refuse `[]`
  là où il attend un dictionnaire. Utiliser `json_map()` avant publication. Côté
  JS, ne jamais écrire une clé texte sur un tableau : `JSON.stringify` la supprime
  silencieusement.
- **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é (voir
  `InteractiveProp._process` et le flash d'attaque dans `enemy.gd`).
- **Les assets de l'outil sont versionnés par `filemtime`** dans `index.php`.
  Sans ça le navigateur sert une version en cache et une correction publiée
  reste invisible — on croit que le code est faux alors qu'il ne s'exécute pas.
- **Tester une fonction ne teste pas son câblage.** Une fonction de rendu peut
  être correcte et n'être jamais appelée. `tests/render_test.js` vérifie les deux.
- **Un test de combat doit imposer ce dont il dépend.** Placer un ennemi à une
  distance fixe suppose une portée d'attaque précise; le placer trop près le met
  en collision avec le joueur, que `move_and_slide` repousse hors de portée.
  Imposer le pattern voulu et se placer relativement à `attack_range`.
- **Ne jamais choisir une entité de test par index.** La composition d'une salle
  est tirée au poids, et un monstre abattu plus tôt reste dans le tableau. Un
  test doit sélectionner par capacité et écarter les instances inactives
  (`_enemy_with_attack_mode` dans le smoke test).
- **Un test qui cherche un nom dans un fichier ne teste pas son exécution.**
  L'assertion « la suppression renumérote la colonne » cherchait
  `renumberColumn(removedColumn)` dans le source : remplacer la garde par
  `if (false)` laissait le test au vert. Vérifier la fonction *et* la forme
  exacte de son appel.
- **Ne jamais figer un numéro de case dans un test.** Le premier test des salles
  alternatives visait la case 3, que la donnée livrée occupait déjà. Choisir la
  case libre en la cherchant.
- **`normalize_amount_map` rend déjà une carte encodable en `{}`.** L'envelopper
  une seconde fois dans `json_map()` lève une TypeError à la publication.
- **Un cache de statistique doit se resynchroniser.** `dungeon.max_stamina`
  était lu une fois au démarrage ; une récompense qui déplace `stamina` le
  laissait périmé. La liste de modificateurs fait foi, un champ n'en est que le
  cache : s'abonner à `state_changed` plutôt que muter.
- **Un formulaire à plusieurs couleurs casse un liant écrit pour une seule.**
  Le champ texte hexadécimal de l'outil visait `item.color` en dur et prenait le
  premier `[data-color-text]` de la page. Les trois couleurs d'un biome
  l'obligent à passer par `data-color-for`.
- **L'état persistant doit être isolé dans le smoke test** : assiettes, compteurs
  par entité, menu, effets. Un reliquat d'une exécution précédente fausse la
  suivante. Voir le bloc `snapshot` / `_restore_progress`.

---

## 7. 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.
- Maintenir l'échelle logique 1920×1080.
- Ajouter une couverture de test proportionnée, puis **vérifier que le test
  attrape vraiment la régression** en cassant temporairement le code. Un test
  qui passe sans jamais avoir échoué ne prouve rien.
- 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 déjà 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é |
| Équipement | prévu (arme, armure, objet, consommables, 1–3 skills, sets
  distincts resto/donjon) mais **non construit** : le socle existe, la
  fonctionnalité reste à faire |
| Dimensions | 4 dans le cycle, plus l'épreuve `fork_trial` hors cycle |
| Verrou | déclaré par la donnée, jamais codé en dur |
| Récompenses | source `run:`, sans remise dans une offre, avec remise entre deux |
| Boss | créature du bestiaire agrandie, pas une seconde table à tenir |
| Salles | la salle est l'unité écrite, le biome n'est qu'une palette |
| 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 » |

### Donjon — construit

Une plongée traverse les biomes d'une dimension puis sa salle de boss. Elle se
termine par : plus de stamina, mise hors service, retour volontaire, ou victoire
sur le boss. Le joueur peut rentrer au restaurant à tout moment.

**Trois portées de persistance** — c'est 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 boss de fin de dimension |
| `meta` | upgrades spéciales, `dimension_index`, `gate_pending`, contenu débloqué | jamais |

La portée est **déclarée par la donnée** (`scope` sur un nœud d'upgrade ou un
fait), 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.

Décisions appliquées : remise à zéro de **tout** l'avancement de la dimension
(économie comprise), upgrades spéciales dans le **même arbre** via `scope`,
restaurant habillé **par données** d'une dimension à l'autre plutôt qu'une scène
par dimension.

**Cycle** : dimension 1 (15 cases) → 2 (20) → 3 (25) → 4 (30, case 26 scellée
par la fourchette maîtresse) → retour à la première. L'épreuve `fork_trial`
(4 cases) accorde la fourchette.

Reste à faire côté donjon : **la plongée ne rapporte pas encore de pièces**, donc
le volet monétaire de l'assurance est implémenté mais jamais sollicité. Les
éléments de salle et les comportements spéciaux sont écrits mais inertes.
L'habillage du restaurant par dimension reste à faire.

Plan détaillé : `docs/architecture-donjon.html`.

Document d'architecture détaillé : `docs/architecture-variables.html`.
