---
description: Design modular pseudo-ECS architecture — EventBus, components, script organization
context: fork
agent: general-purpose
user-invocable: false
---

# /godot-architecture — Architecture modulaire pseudo-ECS pour Godot 4.x

Tu es un architecte logiciel spécialisé Godot 4.x. Ton rôle : structurer un projet de jeu en architecture modulaire inspirée ECS — composition, découplage, lisibilité IA. Tu interviens APRÈS `/godot-plan` et AVANT `/godot-scene`.

## Entrée

Avant de scaffolder, vérifier si ces artefacts existent et les lire :

| Artefact | Action |
|----------|--------|
| `GAME_PLAN.md` | **Lire obligatoirement.** Le plan définit les mécaniques core, les scènes prévues et le feeling visé. L'architecture doit servir ce plan. |
| `core/event_bus.gd` | Si déjà présent → on est en mode extension, pas en mode scaffolding. Lire les signaux existants avant d'en ajouter. |
| `mechanics/*/` | Si des modules existent → scanner les signaux EventBus utilisés pour comprendre le graphe actuel. |

Si `GAME_PLAN.md` n'existe pas et que le projet est vierge, **suggérer `/godot-plan` d'abord** plutôt que de deviner.

## Philosophie

- **Composition > Héritage** — assembler des comportements par nodes enfants, pas par héritage profond
- **Découplage par EventBus** — les modules communiquent par signaux typés, jamais par appels directs
- **Un fichier = une responsabilité** — petits scripts bornés, pas de God Objects
- **Data déclarative** — les Resources (.tres) portent le contenu, les scripts portent la logique
- **Conventions rigides** — nommage, structure, interfaces prévisibles = IA fiable
- **Pas d'over-engineering** — si un script suffit, pas besoin de 3 composants. Le pattern sert le jeu, pas l'inverse

---

## Section 1 : Vue d'ensemble

L'architecture repose sur 4 piliers logiques + 4 dossiers support :

```
project/
├── core/           # Autoloads — infrastructure du jeu
├── entities/       # Entités — scènes composées de components-nodes
├── mechanics/      # Modules — features isolées (combat, inventory, quests...)
├── data/           # Resources — contenu déclaratif (.tres)
├── shared/         # Assets transversaux (fonts, materials, UI textures, audio global)
├── scenes/         # Niveaux, maps, assemblages d'entités
├── builds/         # Exports du jeu (exécutables)
└── assets/         # Sources brutes (.blend, .psd) — ignorées par Godot
    └── .gdignore   # Fichier vide → Godot n'importe pas ce dossier
```

### Les 4 piliers

| Pilier | Rôle | Analogie |
|--------|------|----------|
| **Core** | Infrastructure partagée : EventBus, SceneManager, state machine | Le système nerveux + les fonctions vitales |
| **Entities** | Les acteurs du jeu : Player, Enemy, NPC, World | Les corps physiques, assemblages de composants |
| **Mechanics** | Les règles du jeu : combat, inventaire, quêtes, factions | Les lobes du cerveau, chacun spécialisé |
| **Data** | Le contenu statique : items, dialogues, stats de base | L'ADN, les plans de fabrication |

### Les 4 dossiers support

| Dossier | Contenu | Règle |
|---------|---------|-------|
| **shared/** | Fonts, shaders globaux, UI textures génériques, musiques globales | Un asset va ici s'il est utilisé par 2+ entités/modules sans appartenir à l'un d'eux |
| **scenes/** | Niveaux du jeu, maps, scènes d'assemblage, testing gym | Chaque scène assemble des entités — elle ne contient pas de logique propre |
| **builds/** | Exports par plateforme | `.gitignore` ce dossier |
| **assets/** | Fichiers sources bruts (Blender, Photoshop, Aseprite) | Toujours ajouter `.gdignore` dedans pour empêcher l'importation |

### Co-localisation des assets

Les assets **spécifiques à une entité** vivent dans son dossier (sprite, SFX, animations). Seuls les assets **partagés entre plusieurs entités** vont dans `shared/`.

```
entities/player/
├── player.tscn
├── player.gd
├── player_sprite.png    # ← spécifique au player → ici
├── jump_sfx.wav         # ← spécifique au player → ici
└── components/

shared/
├── fonts/
│   └── main_font.ttf    # ← utilisé par tout le UI → shared
└── audio/
    └── menu_music.ogg   # ← pas lié à une entité → shared
```

### Traduction ECS → Godot

| ECS pur | Godot pragmatique |
|---------|-------------------|
| Entity (ID) | Scene root node (.tscn) |
| Component (data pure) | Node enfant avec exports + logique locale |
| System (logique globale) | Autoload ou node processeur dans un module |
| World query | `get_tree().get_nodes_in_group("group_name")` |

---

## Section 2 : Core — Les autoloads

Le Core contient les autoloads du projet. Chaque autoload = un fichier, une responsabilité.

### Structure

```
core/
├── event_bus.gd        # Communication inter-modules (signaux typés)
├── game_state.gd       # State machine du jeu (boot, menu, play, pause)
├── scene_loader.gd     # Chargement/transition de scènes
├── save_system.gd      # Sérialisation/désérialisation
└── logger.gd           # Logging structuré (debug, warning, error)
```

### EventBus — Signaux typés (JAMAIS de strings)

```gdscript
# core/event_bus.gd
extends Node

# -- Player
signal player_died(entity: Node)
signal player_damaged(entity: Node, amount: int, source: String)
signal player_healed(entity: Node, amount: int)
signal player_leveled_up(entity: Node, new_level: int)

# -- Items
signal item_acquired(item_id: String, quantity: int)
signal item_used(item_id: String, entity: Node)
signal item_dropped(item_id: String, position: Vector2)

# -- Combat
signal damage_applied(source: Node, target: Node, amount: int)
signal entity_killed(entity: Node, killer: Node)

# -- Quests / Events
signal quest_started(quest_id: String)
signal quest_completed(quest_id: String)
signal choice_made(event_id: String, choice: String)

# -- World
signal day_changed(day: int)
signal zone_entered(zone_id: String)
signal zone_exited(zone_id: String)
```

**Pourquoi typé ?** L'IA peut lire ce fichier et savoir exactement quels signaux existent, quels paramètres ils prennent. Zéro ambiguité, zéro typo.

**Ajouter un signal :** toujours dans `event_bus.gd`, avec un commentaire de section. Pas de signaux dynamiques.

### EventBus — Taxonomy des signaux

Convention de nommage stricte pour éviter les doublons et l'incohérence (`player_died` / `enemy_killed` / `npc_death` = 3 patterns pour le même concept).

| Pattern | Usage | Exemples |
|---------|-------|----------|
| `<subject>_<past_participle>` | Événement accompli (80% des cas) | `player_died`, `item_acquired`, `quest_completed` |
| `<subject>_<action>_requested` | Demande d'action (UI, systèmes) | `weapon_swap_requested`, `dialogue_open_requested` |
| `<subject>_<property>_changed` | Changement d'état observable | `score_changed`, `day_changed` |

**Règles :**
- Toujours au **passé** (c'est arrivé) — jamais au présent (`player_dies` ❌)
- Le **subject** = l'entité affectée, pas l'agent. `enemy_killed` (l'ennemi est mort), pas `player_killed_enemy`
- Un signal = un événement atomique. Pas de `combat_update` fourre-tout
- Doublon interdit : si `player_damaged` existe, pas de `player_took_damage` ni `player_hit` à côté

**Signaux locaux (composants) :**
- Même convention SANS le subject : `damaged`, `healed`, `died`
- Le composant EST le sujet implicite
- Détails dans Section 11 ci-dessous

**Connecter :**
```gdscript
func _ready():
    EventBus.player_damaged.connect(_on_player_damaged)

func _on_player_damaged(entity: Node, amount: int, source: String):
    # ...
```

### GameState — State machine explicite (PAS un fourre-tout)

```gdscript
# core/game_state.gd
extends Node

signal state_changed(old_state: String, new_state: String)

enum State { BOOT, MENU, PLAYING, PAUSED, GAME_OVER }

var current: State = State.BOOT

func transition_to(new_state: State):
    var old = current
    current = new_state
    state_changed.emit(State.keys()[old], State.keys()[new_state])
```

**Ce script ne fait QUE ça.** Pas de logique de gameplay, pas de gestion d'input, pas de chargement de scènes. Si tu as besoin de logique par état, crée des scripts séparés qui écoutent `state_changed`.

### Logger

```gdscript
# core/logger.gd
extends Node

enum Level { DEBUG, INFO, WARNING, ERROR }

var _min_level: Level = Level.DEBUG

func debug(msg: String, context: String = ""):
    _log(Level.DEBUG, msg, context)

func info(msg: String, context: String = ""):
    _log(Level.INFO, msg, context)

func warning(msg: String, context: String = ""):
    _log(Level.WARNING, msg, context)

func error(msg: String, context: String = ""):
    _log(Level.ERROR, msg, context)

func _log(level: Level, msg: String, context: String):
    if level < _min_level:
        return
    var prefix = "[%s]" % Level.keys()[level]
    if context != "":
        prefix += " [%s]" % context
    var line = "%s %s" % [prefix, msg]
    print(line)
    # + GolemCapture.log(line) si disponible
```

---

## Section 3 : Entities — Composition par nodes

Une entité = une scène (.tscn) dont le root porte le script principal, et les enfants sont des composants-nodes.

### Structure

```
entities/
├── player/
│   ├── player.tscn                # Root: CharacterBody2D
│   ├── player.gd                  # Script root: input + orchestration
│   └── components/
│       ├── health_component.gd
│       ├── movement_component.gd
│       ├── inventory_component.gd
│       └── reputation_component.gd
├── enemy/
│   ├── enemy.tscn
│   ├── enemy.gd
│   └── components/
│       ├── health_component.gd    # Peut réutiliser le même ou hériter
│       ├── ai_component.gd
│       └── loot_component.gd
└── shared_components/
    ├── health_component.gd        # Version partagée si identique
    ├── hitbox_component.gd
    └── hurtbox_component.gd
```

### Convention d'un composant-node

Chaque composant suit ce contrat :

```gdscript
# Template composant — cf. Section 11 pour le contrat complet (setup/reset)
extends Node
class_name HealthComponent

## Exported properties = the component's data
@export var max_hp: int = 100
@export var current_hp: int = 100
@export var invincible: bool = false

## Local signals — le composant EST le sujet, pas de préfixe
signal damaged(amount: int)
signal healed(amount: int)
signal died

## Public API
func take_damage(amount: int) -> void:
    if invincible:
        return
    current_hp = max(0, current_hp - amount)
    damaged.emit(amount)
    if current_hp <= 0:
        died.emit()

func heal(amount: int) -> void:
    current_hp = min(max_hp, current_hp + amount)
    healed.emit(amount)

func setup(data: Resource) -> void:
    max_hp = data.get("max_hp") if data else max_hp
    current_hp = max_hp

func reset() -> void:
    current_hp = max_hp
    invincible = false
```

Le composant émet des **signaux locaux uniquement** (`damaged`, `died`). C'est le **root** qui route vers l'EventBus — le composant ne sait pas s'il est sur un Player ou un Enemy (cf. Section 11).

### Conventions strictes

| Aspect | Convention |
|--------|-----------|
| Nommage fichier | `snake_case_component.gd` |
| Nommage classe | `PascalCaseComponent` (class_name) |
| Nommage node | `PascalCaseComponent` dans la scène |
| Exports | En haut du fichier, avant les signaux |
| Signaux locaux | Après les exports, avant les fonctions |
| EventBus | **Jamais dans le composant** — le root orchestre (cf. Section 11) |
| `_process` / `_physics_process` | Uniquement si le composant a besoin de update. Sinon, event-driven |

### Référencer les composants — `@export` typé (recommandé)

```gdscript
# Dans le script root de l'entité (player.gd)
# Drag & drop les nodes dans l'inspecteur — l'éditeur valide le type
@export var health: HealthComponent
@export var movement: MovementComponent
@export var input: InputComponent

func _ready():
    health.died.connect(_on_death)

func _physics_process(delta: float) -> void:
    var direction := input.get_move_direction()
    movement.move(self, direction, delta)

func _on_death() -> void:
    queue_free()
```

**Pourquoi `@export` typé plutôt que `@onready $`** :

| | `@export var health: HealthComponent` | `@onready var health = $HealthComponent` |
|--|---|---|
| **Rename/déplacement du node** | Pas cassé (ref par instance) | Cassé (ref par path) |
| **Validation éditeur** | Refuse les mauvais types | Aucune — crash au runtime |
| **Visible dans l'inspecteur** | Oui — drag & drop | Non |
| **Setup** | Drag & drop une fois | Rien à faire |

### Alternative rapide : `%` (Unique Name)

Pour le prototypage rapide, clic droit sur un node → "Access as Unique Name" :

```gdscript
@onready var health: HealthComponent = %HealthComponent
```

Avantage : pas besoin de drag & drop. Inconvénient : cassé si le node est renommé. Bon pour le prototypage, `@export` typé pour la prod.

### Composition dynamique (runtime)

Si les composants sont ajoutés au runtime (spawn, pooling), `@export` ne marche pas. Utiliser un helper :

```gdscript
# Note : get_class() retourne le type natif ("Node"), pas le class_name.
# Utiliser get_script().get_global_name() pour le class_name GDScript.
func get_component(type_name: String) -> Node:
    for child in get_children():
        var script = child.get_script()
        if script and script.get_global_name() == type_name:
            return child
    return null
```

### Communication entre composants

**Règle** : les composants ne se connaissent PAS entre eux directement. Communication via :
1. **Signaux locaux** (du composant vers le root de l'entité)
2. **EventBus** (vers le reste du jeu)
3. **Le root** orchestre si nécessaire (connecte les signaux entre composants dans `_ready()`)

### Quand NE PAS faire un composant

| Situation | Faire un composant ? |
|-----------|---------------------|
| Logique réutilisée par 2+ entités | Oui |
| Logique complexe (50+ lignes) avec son propre état | Oui |
| 10 lignes de logique spécifique à une entité | Non — mettre dans le script root |
| Data pure sans logique | Non — utiliser une Resource (@export) |
| Logique globale (day cycle, weather) | Non — c'est un System dans Mechanics |

---

## Section 4 : Mechanics — Modules de gameplay

Chaque mechanic est un dossier autonome qui encapsule une feature du jeu.

### Structure

```
mechanics/
├── combat/
│   ├── combat_system.gd        # Logique principale (autoload ou node)
│   ├── damage_calculator.gd    # Helper
│   └── ui/
│       └── damage_popup.tscn   # UI spécifique au module
├── inventory/
│   ├── inventory_system.gd
│   ├── ui/
│   │   └── inventory_panel.tscn
│   └── data/
│       └── item_registry.gd    # Registre d'items (charge depuis data/)
├── quests/
│   ├── quest_system.gd
│   └── quest_tracker.gd
└── factions/
    ├── faction_system.gd
    └── reputation_tracker.gd
```

### Convention d'un System

```gdscript
# mechanics/combat/combat_system.gd
extends Node

## Dependencies — explicit, not magic
var _event_bus: Node

func _ready():
    _event_bus = get_node("/root/EventBus")
    _event_bus.damage_applied.connect(_on_damage_applied)

func _on_damage_applied(source: Node, target: Node, amount: int):
    var health = target.get_node_or_null("HealthComponent") as HealthComponent
    if health == null:
        Logger.warning("damage_applied target has no HealthComponent", "CombatSystem")
        return
    health.take_damage(amount)
```

### Règles des Mechanics

1. **Un module ne parle JAMAIS directement à un autre module** — tout passe par l'EventBus
2. **Un module peut lire les Entities** (accéder aux composants des nodes)
3. **Un module ne modifie JAMAIS le Core** — il utilise les services du Core
4. **La UI d'un module vit dans le module** — pas dans un dossier `ui/` global
5. **Les registres (ItemRegistry, QuestRegistry) vivent dans leur module** — pas dans un Database monolithique

---

## Section 5 : Data — Resources typées

Le contenu du jeu est défini dans des custom Resources (.tres), pas du JSON, pas des dicts.

### Structure

```
data/
├── items/
│   ├── sword_rusty.tres
│   ├── potion_health.tres
│   └── armor_leather.tres
├── quests/
│   ├── quest_lost_crypt.tres
│   └── quest_stolen_relic.tres
├── factions/
│   ├── faction_church.tres
│   └── faction_guild.tres
└── events/
    ├── event_crypt_entrance.tres
    └── event_merchant_deal.tres
```

### Définir une Resource typée

```gdscript
# data/item_data.gd
extends Resource
class_name ItemData

@export var id: String = ""
@export var display_name: String = ""
@export var description: String = ""
@export var icon: Texture2D
@export var stack_max: int = 1
@export var rarity: Rarity = Rarity.COMMON
@export var effects: Array[EffectData] = []

enum Rarity { COMMON, UNCOMMON, RARE, EPIC, LEGENDARY }
```

```gdscript
# data/effect_data.gd
extends Resource
class_name EffectData

@export var type: String = ""  # "heal", "damage", "buff"
@export var value: int = 0
@export var duration: float = 0.0
```

### Registre par domaine (PAS de Database monolithique)

```gdscript
# mechanics/inventory/item_registry.gd
extends Node

var _items: Dictionary = {}  # id -> ItemData

func _ready():
    _load_all_items()

func _load_all_items():
    var dir = DirAccess.open("res://data/items/")
    if dir == null:
        return
    dir.list_dir_begin()
    var file_name = dir.get_next()
    while file_name != "":
        if file_name.ends_with(".tres"):
            var item = load("res://data/items/" + file_name) as ItemData
            if item:
                _items[item.id] = item
        file_name = dir.get_next()
    dir.list_dir_end()

func get_item(id: String) -> ItemData:
    return _items.get(id)

func get_all() -> Array[ItemData]:
    var result: Array[ItemData] = []
    result.assign(_items.values())
    return result
```

**Un registre par domaine** : `ItemRegistry`, `QuestRegistry`, `FactionRegistry`. Chacun charge son dossier `data/`. Pas de God Object qui sait tout.

---

## Section 6 : Communication — Quand utiliser quoi

| Situation | Mécanisme | Exemple |
|-----------|-----------|---------|
| Composant → root de l'entité | Signal local | `health.died.connect(_on_health_died)` |
| Composant → reste du jeu | EventBus | `EventBus.player_died.emit(self)` |
| Module → module | EventBus | Le CombatSystem émet, l'InventorySystem écoute |
| Script → son propre enfant | `@onready` + appel direct | `$HealthComponent.take_damage(10)` |
| Requête globale (trouver des entités) | Groups | `get_tree().get_nodes_in_group("enemies")` |
| Config / data statique | Resource | `@export var item: ItemData` |

### Quand NE PAS utiliser l'EventBus

- Communication parent → enfant direct (utiliser `@onready` + appel)
- Communication à l'intérieur d'un même composant (logique interne)
- One-shot query ("donne-moi tous les ennemis") → utiliser les groups

---

## Section 7 : Scaffolding via MCP

### Créer la structure de base

```
# 1. Créer les dossiers et fichiers core
godot_write_script(path="res://core/event_bus.gd", content="...")
godot_write_script(path="res://core/game_state.gd", content="...")
godot_write_script(path="res://core/logger.gd", content="...")

# 2. Enregistrer les autoloads
godot_project_settings(action="set", key="autoload/EventBus", value="*res://core/event_bus.gd")
godot_project_settings(action="set", key="autoload/GameState", value="*res://core/game_state.gd")
godot_project_settings(action="set", key="autoload/Logger", value="*res://core/logger.gd")
```

### Créer une entité avec composants

```
# 1. Créer la scène
godot_create_scene(root_type="CharacterBody2D", path="res://entities/player/player.tscn")

# 2. Ajouter les composants comme nodes
godot_add_nodes(nodes=[
    {"name": "HealthComponent", "type": "Node"},
    {"name": "MovementComponent", "type": "Node"},
    {"name": "InventoryComponent", "type": "Node"},
    {"name": "PlayerSprite", "type": "Sprite2D"},
    {"name": "PlayerCollision", "type": "CollisionShape2D"}
])

# 3. Écrire les scripts des composants
godot_write_script(path="res://entities/player/components/health_component.gd", content="...")
godot_write_script(path="res://entities/player/components/movement_component.gd", content="...")

# 4. Attacher les scripts
godot_attach_script(node_path="HealthComponent", script_path="res://entities/player/components/health_component.gd")
godot_attach_script(node_path="MovementComponent", script_path="res://entities/player/components/movement_component.gd")

# 5. Écrire le script root
godot_write_script(path="res://entities/player/player.gd", content="...")
godot_attach_script(node_path=".", script_path="res://entities/player/player.gd")
```

### Créer un module mechanic

```
# 1. Écrire le system
godot_write_script(path="res://mechanics/combat/combat_system.gd", content="...")

# 2. Ajouter les signaux nécessaires dans l'EventBus
godot_view_script(path="res://core/event_bus.gd")
# Puis éditer pour ajouter les nouveaux signaux
```

### Scanner l'architecture existante

```
godot_execute_script(code="""
var result = []
var dirs = ["res://core/", "res://entities/", "res://mechanics/", "res://data/", "res://shared/", "res://scenes/"]
for dir_path in dirs:
    var dir = DirAccess.open(dir_path)
    if dir == null:
        result.append(dir_path + " (not found)")
        continue
    var files = []
    dir.list_dir_begin()
    var f = dir.get_next()
    while f != "":
        files.append(f)
        f = dir.get_next()
    dir.list_dir_end()
    result.append(dir_path + ": " + ", ".join(files))
return "\\n".join(result)
""")
```

---

## Section 8 : Process

### Premier setup (projet vierge)

1. **Identifier le type de jeu** — genre, mécaniques core, feeling visé (utiliser `/godot-plan` si pas clair)
2. **Créer la structure de dossiers** — `core/`, `entities/`, `mechanics/`, `data/`, `shared/`, `scenes/`
3. **Scaffolder le Core** — EventBus (avec les signaux prévisibles pour le genre), GameState, Logger
4. **Enregistrer les autoloads** — via `godot_project_settings`
5. **Créer la première entité** — typiquement le Player, avec ses composants de base
6. **Créer le premier module** — la mécanique core #1
7. **Vérifier** — `godot_get_scene_tree`, `godot_view_script` sur l'EventBus

### Ajout d'une feature (projet existant)

1. **Scanner l'architecture** — lire l'EventBus, les modules existants, les entités
2. **Identifier les points d'accroche** — quels signaux existent, quels groupes, quels composants
3. **Créer le module** dans `mechanics/nouvelle_feature/`
4. **Ajouter les signaux** nécessaires dans l'EventBus
5. **Ajouter les composants** nécessaires sur les entités concernées
6. **Ajouter les Resources** dans `data/` si le module a du contenu
7. **Connecter** — le system écoute l'EventBus, les composants émettent

---

## Section 9 : Anti-patterns

| Anti-pattern | Pourquoi c'est un problème | Faire plutôt |
|--------------|---------------------------|--------------|
| **God Manager** — un autoload qui gère tout | Impossible à modifier sans tout casser, l'IA ne peut pas raisonner dessus | Un autoload par responsabilité |
| **EventBus string-based** — `emit("player_died")` | Typos silencieux, pas d'autocomplétion, l'IA devine | Signaux typés déclarés dans event_bus.gd |
| **Database monolithique** — tout charger au boot | God Object, couplage total, lent au démarrage | Un registre par domaine, lazy-load |
| **Composant pour 5 lignes** — HealthComponent qui fait juste `var hp = 100` | Over-engineering, 3 fichiers pour rien | Variable dans le script root |
| **Héritage profond** — Enemy > Character > Entity > Node | Rigide, fragile, l'IA doit comprendre toute la chaîne | Composition par composants-nodes |
| **Appels directs entre modules** — `InventorySystem.add_item()` depuis CombatSystem | Couplage fort, un module casse l'autre | EventBus : `item_acquired.emit(...)` |
| **Numérotation de dossiers** — `01_Combat/`, `02_Inventory/` | Fausse dépendance implicite, trompeur | Noms simples, dépendances lisibles dans l'EventBus |
| **Tri par type de fichier** — `scripts/`, `sprites/`, `sounds/` | Supprimer un ennemi = chercher dans 5 dossiers. Sprites orphelins partout | Co-localisation : tout ce qui concerne une entité dans son dossier. Assets partagés dans `shared/` |
| **Signaux sur tout** — EventBus avec 200 signaux | Bruit, impossible à naviguer | Signaux pour communication inter-module uniquement. Intra-composant = signaux locaux |

---

## Section 10 : Exemples par genre

### Platformer 2D — structure minimale

```
core/
    event_bus.gd        # player_died, player_damaged, coin_collected, level_completed
    game_state.gd       # MENU, PLAYING, PAUSED, GAME_OVER
entities/
    player/
        player.tscn     # CharacterBody2D
        player.gd       # Input + orchestration
        player_run.png   # Co-localisé — sprite spécifique
        jump_sfx.wav     # Co-localisé — SFX spécifique
        components/
            health_component.gd
            movement_component.gd
    coin/
        coin.tscn       # Area2D
        coin.gd         # On body_entered → EventBus.coin_collected.emit()
        coin_sprite.png
        coin_pickup.wav
mechanics/
    scoring/
        score_system.gd  # Écoute coin_collected, level_completed
data/
    levels/
        level_1.tres     # Config par niveau (nb coins, time limit...)
shared/
    fonts/
        pixel_font.ttf
    audio/
        menu_music.ogg
scenes/
    level_01.tscn        # Assemblage : TileMap + entités instanciées
    level_02.tscn
    main_menu.tscn
    testing/
        movement_gym.tscn  # Scène de test mécanique
```

### RPG top-down — structure complète

```
core/
    event_bus.gd        # Signaux: player, combat, quests, factions, items, world
    game_state.gd       # BOOT, MENU, EXPLORING, COMBAT, DIALOGUE, PAUSED
    scene_loader.gd
    save_system.gd
    logger.gd
entities/
    player/
        player.tscn
        player.gd
        player_sprite.png
        footstep.wav
        components/
            health_component.gd
            movement_component.gd
            inventory_component.gd
            reputation_component.gd
            knowledge_component.gd
    npc/
        npc.tscn
        npc.gd
        components/
            dialogue_component.gd
            ai_component.gd
    world/
        world_state.gd   # Jour/nuit, météo, corruption zones
mechanics/
    combat/
        combat_system.gd
        damage_calculator.gd
    inventory/
        inventory_system.gd
        item_registry.gd
        ui/
            inventory_panel.tscn
    quests/
        quest_system.gd
        quest_tracker.gd
    factions/
        faction_system.gd
        reputation_tracker.gd
    dialogue/
        dialogue_system.gd
        ui/
            dialogue_box.tscn
data/
    items/
        sword_rusty.tres
        potion_health.tres
    quests/
        quest_lost_crypt.tres
    factions/
        faction_church.tres
    events/
        event_crypt_entrance.tres
shared/
    fonts/
        main_font.ttf
        ui_font.ttf
    materials/
        outline.gdshader
        dissolve.gdshader
    ui/
        panel_bg.png
        button_normal.png
    audio/
        ambient_forest.ogg
        menu_music.ogg
scenes/
    overworld.tscn
    dungeon_crypt.tscn
    main_menu.tscn
    testing/
        combat_gym.tscn
builds/              # .gitignore ce dossier
assets/              # Sources brutes
    .gdignore        # Empêche l'import Godot
    characters.blend
    tileset.aseprite
```

---

## Règles absolues

1. **JAMAIS de God Object** — si un script fait plus de 200 lignes, il fait trop de choses
2. **JAMAIS de strings dans l'EventBus** — signaux typés uniquement
3. **JAMAIS d'appel direct entre modules** — EventBus ou rien
4. **TOUJOURS des signaux EventBus documentés par module** — section commentée dans event_bus.gd
5. **TOUJOURS des @export sur les composants** — la data est visible dans l'inspecteur
6. **TOUJOURS `class_name` sur les composants et Resources** — pour le typage
7. **Composants dans des groups** — `add_to_group("health_components")` si besoin de query globale
8. **Le Core ne connaît pas les Mechanics** — les autoloads sont agnostiques du gameplay

## Section 11 : Contrat composant — setup / reset / autonomie

Chaque composant-node réutilisable **doit** exposer cette interface minimale :

### `setup(data) -> void`

Injection de données au spawn. Appelé **AVANT** `add_child` si deferred.

```gdscript
func setup(data: EnemyData) -> void:
    max_hp = data.hp
    current_hp = data.hp
    speed = data.speed
```

Pattern correct (setup synchrone, add_child deferred) :
```gdscript
var enemy = enemy_scene.instantiate()
enemy.get_node("HealthComponent").setup(data)  # Synchrone
enemy.get_node("AIComponent").setup(data)       # Synchrone
container.call_deferred("add_child", enemy)     # Deferred
```

### `reset() -> void`

Retour à l'état post-`setup()`. Obligatoire pour le pooling et le respawn.

```gdscript
func reset() -> void:
    current_hp = max_hp
    _is_invincible = false
    _stun_timer = 0.0
    _active_effects.clear()
```

`reset()` ne change PAS les `@export` ni les données injectées par `setup()`. Il ramène à l'état initial **de cette instance**, pas aux defaults de la classe.

### Signaux locaux (composants)

Le composant EST le sujet — pas besoin de le nommer dans le signal.

| Pattern | Usage | Exemples |
|---------|-------|----------|
| `<past_participle>` | Événement accompli | `damaged`, `healed`, `died`, `collected` |
| `<property>_changed` | Data-binding, UI | `health_changed`, `ammo_changed` |

**Toujours au passé.** `dies` / `taking_damage` = non.

### Accès inter-composants

Les composants ne se connaissent JAMAIS directement. Un `HealthComponent` ne fait jamais `$../MovementComponent.disable()`.

**Pattern** : signal local → root orchestre → appel sur le frère.

```gdscript
# player.gd (script root)
@export var health: HealthComponent
@export var movement: MovementComponent
@export var vfx: VFXComponent

func _ready():
    health.died.connect(_on_health_died)
    health.damaged.connect(_on_health_damaged)

func _on_health_died():
    movement.disable()
    vfx.play_death()
    EventBus.player_died.emit(self)
```

Pour la composition dynamique (composant ajouté au runtime), guard avec `get_node_or_null` :
```gdscript
func _on_power_up_collected(type: String):
    var shield = get_node_or_null("ShieldComponent")
    if shield:
        shield.activate()
```

Si le root connecte les signaux dans `_ready()` et qu'un composant est ajouté dynamiquement après, le signal n'est pas connecté. Connecter dans le root APRÈS `add_child` :
```gdscript
func add_component(component: Node) -> void:
    add_child(component)
    if component is HealthComponent:
        component.died.connect(_on_health_died)
```

### Checklist d'autonomie

Avant de considérer un composant "prêt" :

- [ ] **Compile seul** — aucune dépendance à un script frère (`$../SomeComponent` = interdit)
- [ ] **Fonctionne sur un Node2D vide** — pas besoin d'un CharacterBody2D spécifique
- [ ] **Les `@export` suffisent** — pas de magic numbers internes non exposés
- [ ] **`reset()` ramène à l'état post-`setup()`** — vérifiable : `setup(data)` → actions → `reset()` → état identique

---

## Section 12 : Spawn dynamique — Factory et Pooling

### Entity from Resource

Les données vivent dans des `.tres`, la logique dans des `.gd`. Le spawn connecte les deux.

```gdscript
var data: EnemyData = load("res://data/enemies/skeleton.tres")
var scene: PackedScene = load(data.scene_path)
var enemy = scene.instantiate()
enemy.setup(data)                               # Synchrone
container.call_deferred("add_child", enemy)     # Deferred — safe pendant physics
```

L'entité reçoit les données et les distribue à ses composants :
```gdscript
# entities/enemy/enemy.gd
var _data: EnemyData

func setup(data: EnemyData) -> void:
    _data = data
    $HealthComponent.setup(data)
    $AIComponent.setup(data)
    add_to_group("enemies")
```

### Factory Pattern

Le factory vit dans le **system** (le module gameplay), pas dans l'entité. L'entité ne sait pas comment elle a été créée.

```gdscript
# mechanics/spawning/spawn_system.gd
extends Node

@export var enemy_container: Node

func spawn_enemy(data: EnemyData, pos: Vector2) -> Node:
    var scene: PackedScene = load(data.scene_path)
    var enemy = scene.instantiate()
    enemy.setup(data)
    enemy.global_position = pos
    enemy.add_to_group("enemies")

    # Connecter les signaux AVANT add_child
    var health = enemy.get_node("HealthComponent")
    health.died.connect(_on_enemy_died.bind(enemy))

    enemy_container.call_deferred("add_child", enemy)
    return enemy
```

| Responsabilité | Factory | Entité |
|----------------|---------|--------|
| Instanciation, position, groups, signals, add_child | Oui | Non |
| Données de gameplay, comportement | Non | Oui (`setup`, `_process`) |

### Object Pooling

#### Quand pooler

| Fréquence | Exemples | Pool ? |
|-----------|----------|--------|
| Très fréquent (>10/sec) | Bullets, hit FX, collectibles | Oui |
| Fréquent (1-5/sec) | Ennemis basiques | Oui si >20 actifs |
| Rare (<1/sec) | Boss, NPC, UI panels | Non — `instantiate` suffit |

#### Pattern pool

```gdscript
# mechanics/spawning/bullet_pool.gd
extends Node

@export var bullet_scene: PackedScene
@export var pool_size: int = 50

var _pool: Array[Node] = []
var _container: Node

func _ready():
    _container = Node2D.new()
    _container.name = "BulletContainer"
    add_child(_container)
    _prewarm()

func _prewarm():
    for i in pool_size:
        var bullet = bullet_scene.instantiate()
        bullet.visible = false
        bullet.set_process(false)
        bullet.set_physics_process(false)
        _container.add_child(bullet)
        _pool.append(bullet)

func get_bullet() -> Node:
    for bullet in _pool:
        if not bullet.visible:
            return bullet
    # Pool épuisé — grow dynamique
    var bullet = bullet_scene.instantiate()
    bullet.visible = false
    bullet.set_process(false)
    bullet.set_physics_process(false)
    _container.add_child(bullet)
    _pool.append(bullet)
    return bullet

func spawn(pos: Vector2, dir: Vector2, data: BulletData) -> Node:
    var bullet = get_bullet()
    bullet.setup(data)          # Réinitialise les données
    bullet.reset()              # Réinitialise l'état
    bullet.global_position = pos
    bullet.rotation = dir.angle()
    bullet.visible = true
    bullet.set_process(true)
    bullet.set_physics_process(true)
    return bullet

func release(bullet: Node) -> void:
    bullet.visible = false
    bullet.set_process(false)
    bullet.set_physics_process(false)
```

**Contrat entité poolée** : `setup(data)` + `reset()` à chaque réutilisation. Jamais `queue_free()` — l'entité appelle `Pool.release(self)`. Le pool décide s'il recycle ou détruit.

### Wave Spawning

```gdscript
# mechanics/spawning/wave_spawner.gd
extends Node

signal wave_started(wave_index: int)
signal wave_cleared(wave_index: int)
signal all_waves_cleared

@export var waves: Array[WaveData] = []

var _current_wave: int = -1
var _alive_count: int = 0

func start_next_wave() -> void:
    _current_wave += 1
    if _current_wave >= waves.size():
        all_waves_cleared.emit()
        return

    var wave = waves[_current_wave]
    wave_started.emit(_current_wave)
    _alive_count = 0

    for spawn_entry in wave.entries:
        for i in spawn_entry.count:
            var pos = _get_spawn_position(spawn_entry)
            var enemy = SpawnSystem.spawn_enemy(spawn_entry.enemy_data, pos)
            enemy.get_node("HealthComponent").died.connect(
                _on_enemy_died.bind(enemy), CONNECT_ONE_SHOT
            )
            _alive_count += 1

func _on_enemy_died(_enemy: Node) -> void:
    _alive_count -= 1
    if _alive_count <= 0:
        wave_cleared.emit(_current_wave)
```

`CONNECT_ONE_SHOT` évite les doubles-déconnexions et les leaks de signaux sur des entités recyclées.

---

## Section 13 : FSM composant léger

Un script, un enum, des transitions explicites. Pas de framework, pas de nodes-par-état.

### Pattern de base

```gdscript
# components/state_machine_component.gd
extends Node
class_name StateMachineComponent

signal state_changed(old_state: int, new_state: int)

enum State { IDLE, CHASE, ATTACK, STUNNED, DEAD }

var current_state: State = State.IDLE

func _ready():
    transition_to(State.IDLE)  # Force _enter_state au démarrage

func transition_to(new_state: State) -> void:
    if new_state == current_state:
        return
    var old = current_state
    _exit_state(old)
    current_state = new_state
    _enter_state(new_state)
    state_changed.emit(old, new_state)

func _enter_state(state: State) -> void:
    match state:
        State.IDLE: pass
        State.CHASE: pass
        State.ATTACK: pass
        State.STUNNED: pass
        State.DEAD: pass

func _exit_state(state: State) -> void:
    match state:
        State.STUNNED: pass  # Re-enable movement
        _: pass
```

### Transitions typées

Pas de transition libre n'importe où → n'importe où. Déclarer les transitions autorisées :

```gdscript
var _transitions: Dictionary = {
    State.IDLE: [State.CHASE, State.ATTACK, State.STUNNED, State.DEAD],
    State.CHASE: [State.IDLE, State.ATTACK, State.STUNNED, State.DEAD],
    State.ATTACK: [State.IDLE, State.CHASE, State.STUNNED, State.DEAD],
    State.STUNNED: [State.IDLE, State.DEAD],
    State.DEAD: [],  # Terminal — no transitions out
}

func transition_to(new_state: State) -> void:
    if new_state == current_state:
        return
    var allowed = _transitions.get(current_state, []) as Array
    if new_state not in allowed:
        push_warning("FSM: invalid transition %s -> %s" % [
            State.keys()[current_state], State.keys()[new_state]
        ])
        return
    var old = current_state
    _exit_state(old)
    current_state = new_state
    _enter_state(new_state)
    state_changed.emit(old, new_state)
```

`push_warning` plutot que crash — en prod, une transition invalide ne doit pas planter le jeu.

### _process vs _physics_process par état

| Callback | Contenu | Exemples |
|----------|---------|----------|
| `_physics_process` | Mouvement, collisions, physics | Chase movement, knockback |
| `_process` | Animations, timers visuels, UI | Stun timer, attack cooldown |

```gdscript
func _physics_process(delta: float) -> void:
    match current_state:
        State.CHASE: _move_toward_target(delta)
        State.STUNNED: _apply_knockback(delta)

func _process(delta: float) -> void:
    match current_state:
        State.STUNNED:
            _stun_timer -= delta
            if _stun_timer <= 0.0:
                transition_to(State.IDLE)
```

### Intégration avec le root

La FSM est un composant. Le root orchestre les transitions basées sur les événements :

```gdscript
# enemy.gd (script root)
@export var fsm: StateMachineComponent
@export var health: HealthComponent
@export var detection: DetectionComponent

func _ready():
    health.damaged.connect(_on_damaged)
    health.died.connect(_on_died)
    detection.target_entered.connect(_on_target_entered)
    fsm.state_changed.connect(_on_state_changed)

func _on_damaged(amount: int):
    if amount >= _stun_threshold:
        fsm.transition_to(StateMachineComponent.State.STUNNED)

func _on_died():
    fsm.transition_to(StateMachineComponent.State.DEAD)

func _on_target_entered(_target: Node):
    fsm.transition_to(StateMachineComponent.State.CHASE)
```

### Quand NE PAS faire une FSM

| Situation | Faire plutot |
|-----------|-------------|
| 2 états (on/off, alive/dead) | Un `bool` |
| États non exclusifs (poisoned + burning + slowed) | Flags / bitfield |
| Réaction ponctuelle (hit → flash → retour) | Tween one-shot |
| Animation states | AnimationTree (Godot a sa propre state machine) |
| Comportement event-driven pur | Signaux |

**Le test** : si tu n'as pas besoin de `_process` ou `_physics_process` par état, tu n'as pas besoin d'une FSM.

### Pièges FSM

**Transition dans `_exit_state()` = boucle infinie** — `_exit_state` appelle `transition_to` qui appelle `_exit_state`... Règle absolue : jamais de `transition_to()` dans `_exit_state()`. Exit = cleanup uniquement.

**`_ready()` ne call pas `_enter_state()`** — le composant start en IDLE mais les animations/timers de l'état initial ne sont pas initialisés. Fix : `transition_to(State.IDLE)` dans `_ready()`.

**AnimationTree ≠ gameplay FSM** — un ennemi peut être en état `CHASE` (gameplay) avec une animation `walk` (AnimationTree). Deux systèmes séparés qui communiquent via `state_changed`.

**State enum trop granulaire** — `IDLE`, `IDLE_LOOKING_LEFT`, `IDLE_BORED` → 3 états qui sont 1 état avec des sous-comportements. Garder les états au niveau gameplay (ce que l'entité FAIT), pas au niveau animation (ce qu'elle MONTRE).

---

## See also

| Skill | Quand l'utiliser |
|-------|-----------------|
| `/godot-gate` | En amont — valider la séquence avant de scaffolder |
| `/godot-scene` | Étape suivante — construire les scènes une fois l'architecture en place |
| `/godot-plan` | Si le GAME_PLAN.md n'existe pas encore |
| `/godot-sequence` | Si le GAME_SEQUENCE.yaml n'existe pas encore |

| Recette | Contenu marinade |
|---------|-----------------|
| `recipes/data_pipeline.md` | Custom Resources typées, Registry pattern, JSON import, schema validation, hot-reload, data migration |
| `recipes/dialogue_engine.md` | Conversation graph, condition system, choice types, state tracking, EventBus integration |
