---
description: Build a Godot scene from a description using MCP tools
user-invocable: false
---
# /godot-scene — Construire une scène Godot à partir d'une description

> **Synonymes** : scene, scène, level, niveau, build, construire, créer, create, nodes, hiérarchie, tscn, instantiate, prefab

Tu es un scene builder Godot 4.x. Le user décrit ce qu'il veut, tu le construis avec les tools MCP.

## Entrée

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

| Artefact | Action |
|----------|--------|
| `GAME_PLAN.md` | Si présent → lire la section "Scènes" et "Hiérarchie de nodes" pour respecter le plan. |
| `core/event_bus.gd` | Si présent → le projet suit une architecture pseudo-ECS. Les scripts doivent utiliser l'EventBus pour la communication inter-modules, et les entités doivent être composées de components-nodes dans `entities/`. |
| `theme.tres` | Si présent → ne pas mettre de `add_theme_*_override`, le Theme gère le style. |

Si aucun artefact n'existe et que la scène est complexe, **suggérer `/godot-plan` d'abord**.

## Process

1. **Analyze the request** — Determine the scene type and pick the right architecture:
   - **2D Game scene** → Root: `Node2D`. Children: game objects as `CharacterBody2D`, `StaticBody2D`, `Area2D`, etc.
   - **3D Game scene** → Root: `Node3D`. Children: `CharacterBody3D`, `MeshInstance3D`, etc.
   - **UI / Menu** → Root: `Control`. Use containers (`VBoxContainer`, `HBoxContainer`, `CenterContainer`, `MarginContainer`) for layout. NEVER position UI elements manually.
   - **Level** → Root: `Node2D` or `Node3D`. TileMapLayer for 2D levels, GridMap for 3D.
   - **Mixed (game + UI overlay)** → Root: `Node2D`/`Node3D` with a `CanvasLayer` child for UI.

2. **Create the scene** — Use `godot_create_scene` with the appropriate root type and path.

3. **Build the node hierarchy** — Use `godot_add_nodes` (batch) to create the entire tree in one call. Always prefer batch over individual `godot_add_node` calls.

4. **Write scripts** — Use `godot_write_script` to create .gd files, then `godot_attach_script` to bind them.

5. **Verify** — Use `godot_get_scene_tree` to confirm the structure is correct.

## Node hierarchy rules

### Every physics body MUST have a CollisionShape child
```
CharacterBody2D
  ├── CollisionShape2D (with a shape resource)
  ├── Sprite2D or AnimatedSprite2D
```

### Owner is set automatically by add_node/add_nodes — don't worry about it.
**Exception**: when instancing a PackedScene via script (`execute_script`), `set_owner` must be called recursively on ALL descendants — see "Instance a PackedScene as sub-scene" below.

### Naming conventions
- PascalCase for node names: `PlayerSprite`, `EnemyCollision`, `MainMenu`
- Scripts match their node: `player.gd` for `Player`, `main_menu.gd` for `MainMenu`
- Scene files: `res://scenes/scene_name.tscn`
- Scripts: `res://scripts/script_name.gd` or alongside the scene

## Common patterns

### Platformer character
```json
{"name": "Player", "type": "CharacterBody2D", "children": [
  {"name": "PlayerSprite", "type": "Sprite2D"},
  {"name": "PlayerCollision", "type": "CollisionShape2D"},
  {"name": "Camera", "type": "Camera2D"}
]}
```

### UI Menu (ALWAYS use containers)
```json
{"name": "Menu", "type": "CenterContainer", "children": [
  {"name": "VBox", "type": "VBoxContainer", "children": [
    {"name": "Title", "type": "Label"},
    {"name": "PlayButton", "type": "Button"},
    {"name": "QuitButton", "type": "Button"}
  ]}
]}
```

### Top-down game
```json
[
  {"name": "Player", "type": "CharacterBody2D", "children": [
    {"name": "PlayerSprite", "type": "Sprite2D"},
    {"name": "PlayerCollision", "type": "CollisionShape2D"}
  ]},
  {"name": "Enemies", "type": "Node2D"},
  {"name": "UI", "type": "CanvasLayer", "children": [
    {"name": "HUD", "type": "Control", "children": [
      {"name": "HealthBar", "type": "ProgressBar"},
      {"name": "ScoreLabel", "type": "Label"}
    ]}
  ]}
]
```

## Properties via update_property

### Setting a Resource (e.g. collision shape):
```
godot_update_property(path="Player/PlayerCollision", property="shape", value={"type": "RectangleShape2D", "properties": {"size": [32, 64]}})
```

### Setting a color:
```
godot_update_property(path="Background", property="color", value="#1a1a2e")
```

### Setting a Vector2:
```
godot_update_property(path="Player", property="position", value=[100, 300])
```

## Script template

When writing scripts, always include:
- `extends` matching the node type
- Clear variable declarations at the top
- `_ready()` for initialization
- `_process(delta)` or `_physics_process(delta)` as needed
- Use `GolemCapture.game_log()` instead of `print()` for output that should be visible via `godot_get_logs`

## Camera2D gotchas

Pour les jeux 2D avec viewport fixe (Pong, puzzle, etc.), la Camera2D a des comportements non-intuitifs :

| Situation | Problème | Solution |
|-----------|----------|----------|
| Contenu de (0,0) à (viewport_size) | Avec `anchor_mode = DRAG_CENTER` (défaut), une caméra à (0,0) montre les coordonnées négatives | Soit positionner la caméra au centre du viewport `(viewport_width/2, viewport_height/2)`, soit utiliser `anchor_mode = FIXED_TOP_LEFT` |
| Jeu fixe sans scrolling | La caméra suit sa position, décale tout | `anchor_mode = FIXED_TOP_LEFT` + position `(0,0)` → le coin (0,0) du jeu = coin haut-gauche de l'écran |
| Screenshake | Modifier `position` directement casse le cadrage | Utiliser `offset` pour le shake, pas `position` |

```gdscript
# Pour un jeu 2D fixe avec screenshake
extends Camera2D

func _ready():
    anchor_mode = Camera2D.ANCHOR_MODE_FIXED_TOP_LEFT
    position = Vector2.ZERO

func shake(intensity: float):
    offset = Vector2(randf_range(-1, 1), randf_range(-1, 1)) * intensity
```

## Canvas positioning — where things appear on screen

L'origin `(0, 0)` en Godot 2D = **coin haut-gauche**. L'axe Y va vers le **bas**.

| Position voulue | Formule | Exemple (1920×1080) |
|-----------------|---------|---------------------|
| Centre de l'écran | `viewport_size / 2` | `(960, 540)` |
| Coin haut-gauche | `(0, 0)` | `(0, 0)` |
| Coin bas-droit | `viewport_size` | `(1920, 1080)` |
| Centre horizontal, 80% bas | `(viewport_w / 2, viewport_h * 0.8)` | `(960, 864)` |

### Patterns de placement via MCP

```
# Centrer le player
godot_update_property(path="Player", property="position", value=[960, 540])

# HUD → toujours dans un CanvasLayer (indépendant de la caméra)
godot_add_node(parent=".", name="UILayer", type="CanvasLayer")

# Background couvrant tout le viewport
godot_update_property(path="Background", property="position", value=[0, 0])
godot_update_property(path="Background", property="size", value=[1920, 1080])
```

### Règle

**Si tu places un game node (Player, Enemy, Spawn) à `(0, 0)`, STOP** — demande-toi si le coin haut-gauche est vraiment la position voulue. Pour les entités gameplay, le centre du viewport est presque toujours le bon choix initial.

## Collision layers convention

| Layer | Usage | Exemple |
|-------|-------|---------|
| 1 | Player | CharacterBody2D du joueur |
| 2 | Enemies | Tous les ennemis |
| 3 | World | StaticBody2D, murs, sol |
| 4 | Projectiles | Balles, flèches, sorts |
| 5 | Pickups | Items collectables, loot |
| 6 | Triggers | Area2D zones (spawn, dialogue, transition) |

### Layer vs Mask

- **`collision_layer`** = "je suis SUR ces layers" (identité)
- **`collision_mask`** = "je DÉTECTE ces layers" (perception)

Exemple : un ennemi (layer 2) qui détecte le player (mask 1) et le monde (mask 3).

### ATTENTION : bitmask ≠ numéro de layer

```
# MAUVAIS — collision_mask = 4 active le layer 3 (bit 3), PAS le layer 4
collision_mask = 4

# BON — utiliser les setters par numéro de layer
godot_update_property(path="Enemy", property="collision_layer", value=2)
```

En GDScript, préférer les setters explicites :
```gdscript
# set_collision_layer_value(layer_number, enabled)
set_collision_layer_value(2, true)   # Je suis un ennemi
set_collision_mask_value(1, true)    # Je détecte le player
set_collision_mask_value(3, true)    # Je détecte le monde
```

## Instance a PackedScene as sub-scene (editor context)

When adding an existing `.tscn` as a sub-scene via `execute_script`, use this pattern:

```gdscript
# 1. Load and instance with editor state
var packed: PackedScene = load("res://scenes/dungeon.tscn")
var instance: Node = packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)

# 2. Add to scene tree
var root: Node = get_editor_interface().get_edited_scene_root()
root.add_child(instance)

# 3. CRITICAL — set_owner recursively on ALL descendants
# Without this, save_scene loses children silently
var stack: Array[Node] = [instance]
while stack.size() > 0:
    var node: Node = stack.pop_back()
    node.owner = root
    for child in node.get_children():
        stack.push_back(child)

# 4. Optionally remove unwanted nodes (e.g. duplicate Camera3D)
# Use .free() NOT queue_free() — queue_free waits end of frame,
# save_scene in the same frame may still see the node
var cam: Node = instance.get_node_or_null("Camera3D")
if cam:
    cam.free()

# 5. Save
get_editor_interface().save_scene()
```

### Key warnings

- **`set_owner` must be recursive** — `instance.owner = root` only sets it on the root of the sub-scene. All
descendants need it too, or `save_scene` silently drops them.
- **Use iterative traversal** for large scenes (tested with 11,292 nodes). Recursive `set_owner_recursive()` can hit stack overflow.
- **`.free()` not `queue_free()`** in editor context — `queue_free()` defers to end of frame. If `save_scene` runs in the same frame, the deleted node may still be present.
- **`GEN_EDIT_STATE_INSTANCE`** keeps the sub-scene as a reference (not flattened inline). Without it, the sub-scene nodes are duplicated directly into the parent `.tscn`.

## After building

1. Run `godot_get_scene_tree` to verify
2. Run `godot_save_scene` to persist
3. If it's the main scene, set it via `godot_project_settings(action="set", key="application/run/main_scene", value="res://scenes/...")`

## Rappels GDScript

- **Ne pas éditer `.tscn` directement** — utiliser `godot_add_node` / `godot_update_property` + `godot_save_scene`
- **FBX/GLB imported scenes : `update_property` + `save_scene` ne persiste PAS les overrides** — `inspect_node` confirme (mémoire éditeur) mais au runtime c'est l'ancienne valeur. Fix : setter dans `_ready()` du script GDScript. Diagnostic : `grep property_name file.tscn` — si absent, l'override ne sera pas au runtime
- **`:=` interdit sur sources Variant** — `var x: Type = expression` si la source est Array, Dict, ternaire, `.duplicate()`
- **`call_deferred("add_child", node)`** si l'ajout se fait pendant un signal physics (sinon crash)
- **`global_position` avant `add_child()` = ignoré** — stocker dans une var, appliquer dans `_ready()`
- **`godot_edit_script` sur Windows** : matcher sur **une seule ligne** (CRLF casse les matches multi-lignes). Préférer `godot_write_script` pour les gros changements

## See also

| Skill | Quand l'utiliser |
|-------|-----------------|
| `/godot-architecture` | En amont — structure projet (EventBus, modules) avant la scène |
| `/godot-composition` | Étape suivante — structurer les UI dans la scène |
| `/godot-test` | Après construction — vérifier que la scène fonctionne |
| `/godot-debug` | Si la scène a des problèmes visuels ou de comportement |

| Recette | Contenu marinade |
|---------|-----------------|
| `recipes/screen_transitions.md` | Shader transitions (iris, diamond, dissolve), Zelda door pattern, scene loading masking |
| `recipes/viewport_patterns.md` | SubViewport, split screen, minimap, pixel art rendering, 3D-in-UI, ViewportTexture |
| `recipes/fbx_retargeting.md` | FBX cross-skeleton retargeting, BoneMap, track remap, T-pose diagnostic, Synty conventions |
