# 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
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** : `dungeon.gd`, `hud.gd`, l'arbre d'upgrades.

---

## 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 8**, **sauvegarde 6**.
Contenu : 5 ingrédients, 5 plats, 5 monstres, 3 profils clients, 15 nœuds
d'upgrade, 14 statistiques, 6 facts, 179 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.

---

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

### Donjon — vision arrêtée, construction en cours

Le donjon devient une progression : une plongée traverse un nombre de salles
fixé d'avance (croissant avec les déblocages et le monde atteint), puis une
salle de boss. Battre ce boss termine le monde. Le joueur peut rentrer au
restaurant à tout moment.

**Trois portées de persistance** — c'est le point structurant :

| Portée | Contient | Effacée |
|---|---|---|
| `run` | salle courante, bonus de plongée | à chaque retour au restaurant |
| `world` | upgrades générales, pièces, ingrédients, compteurs, restaurant | à la victoire sur le boss de fin de monde |
| `meta` | upgrades spéciales, `world_index`, 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 `world` : un oubli ne rend jamais un acquis
permanent par accident. `GameState.end_run()` et `complete_world()` sont les
deux seules opérations.

Décisions prises : remise à zéro de **tout** l'avancement du monde (économie
comprise), upgrades spéciales dans le **même arbre** via `scope`, restaurant
habillé **par données** d'un monde à l'autre plutôt qu'une scène par monde.

Reste à faire : apparition pondérée (`spawn_count` → `spawn_weight`), salles et
porte, renforts, menu de récompenses, assurance à la mise hors service, biomes
et salles de boss, cycle des mondes.

Plan détaillé : `docs/architecture-donjon.html`.

Document d'architecture détaillé : `docs/architecture-variables.html`.
