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

> **Synonymes** : architecture, archi, ECS, EventBus, structure, organiser, refactoring, refacto, modules, components, composants, découplage, decoupling, scalable, maintenable

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 leurs manifestes `_meta` 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 :

```
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)
```

| 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 |

### 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_dealt(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.

**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
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 = what this component broadcasts
signal damaged(amount: int)
signal healed(amount: int)
signal died

## Public API = how other components interact with this one
func take_damage(amount: int) -> void:
    if invincible:
        return
    current_hp = max(0, current_hp - amount)
    damaged.emit(amount)
    EventBus.player_damaged.emit(get_parent(), amount, "")
    if current_hp <= 0:
        died.emit()
        EventBus.player_died.emit(get_parent())

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

### 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 | Connecté dans `_ready()`, émis dans les méthodes publiques |
| `_process` / `_physics_process` | Uniquement si le composant a besoin de update. Sinon, event-driven |

### Accéder à un composant frère

```gdscript
# Dans le script root de l'entité (player.gd)
@onready var health: HealthComponent = $HealthComponent
@onready var movement: MovementComponent = $MovementComponent

# OU via un helper si composition dynamique
func get_component(type: String) -> Node:
    for child in get_children():
        if child.get_class() == type or (child.has_method("get_component_name") and child.get_component_name() == type):
            return child
    return null
```

**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/
│   ├── _meta.tres              # Manifeste du module
│   ├── combat_system.gd        # Logique principale (autoload ou node)
│   ├── damage_calculator.gd    # Helper
│   └── ui/
│       └── damage_popup.tscn   # UI spécifique au module
├── inventory/
│   ├── _meta.tres
│   ├── inventory_system.gd
│   ├── ui/
│   │   └── inventory_panel.tscn
│   └── data/
│       └── item_registry.gd    # Registre d'items (charge depuis data/)
├── quests/
│   ├── _meta.tres
│   ├── quest_system.gd
│   └── quest_tracker.gd
└── factions/
    ├── _meta.tres
    ├── faction_system.gd
    └── reputation_tracker.gd
```

### Manifeste (`_meta.tres`)

Chaque module a un manifeste Resource qui déclare ce qu'il fait et de quoi il dépend :

```gdscript
# mechanics/combat/_meta.tres (créé via script)
# Ou en GDScript :
# mechanics/combat/combat_meta.gd
extends Resource
class_name ModuleMeta

@export var module_name: String = "Combat"
@export var description: String = "Damage calculation, hitboxes, death handling"
@export var depends_on: PackedStringArray = ["core/event_bus"]
@export var listens_to: PackedStringArray = ["damage_dealt", "entity_killed"]
@export var emits: PackedStringArray = ["player_damaged", "player_died"]
```

**Pourquoi ?** L'IA scanne les manifestes pour comprendre le graphe de dépendances sans lire le code de chaque module.

### 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_dealt.connect(_on_damage_dealt)

func _on_damage_dealt(source: Node, target: Node, amount: int):
    var health = target.get_node_or_null("HealthComponent") as HealthComponent
    if health == null:
        Logger.warning("damage_dealt 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. Écrire le manifeste
godot_write_script(path="res://mechanics/combat/combat_meta.gd", content="...")

# 3. 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/"]
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/`
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. **Écrire le manifeste** `_meta` du module
8. **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 dans le manifeste |
| **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
        components/
            health_component.gd
            movement_component.gd
    coin/
        coin.tscn       # Area2D
        coin.gd         # On body_entered → EventBus.coin_collected.emit()
mechanics/
    scoring/
        score_system.gd  # Écoute coin_collected, level_completed
data/
    levels/
        level_1.tres     # Config par niveau (nb coins, time limit...)
```

### 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
        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
```

---

## 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 un manifeste par module** — même minimal
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
