---
paths:
  - "**/*.gd"
  - "godot-plugin/**"
---

# GDScript — Timing, Types & Patterns

## Timing & Signals

- **Signaux = synchrones** — `emit()` exécute tous les handlers IMMÉDIATEMENT avant la ligne suivante. Pattern : suppress flag + `call_deferred("set", "_flag", false)` pour reset au frame suivant
- **`global_position` avant `add_child()` = ignoré** — stocker dans une var via `setup()`, appliquer dans `_ready()`. `position` (locale) OK si le parent est à (0,0)
- **`set_script()` avant `add_child()` = `@onready` OK** — le script est attaché avant l'entrée dans l'arbre
- **`queue_free()` n'est pas immédiat** — le node existe encore pendant le frame. Toujours `is_instance_valid(node)` et `node.is_queued_for_deletion()` avant usage
- **Guard après CHAQUE `await`** — n'importe quel node peut avoir été freed. `is_instance_valid()` sur self ET objets référencés
- **`add_child()` pendant `_ready()` parent = erreur** — le parent setup ses enfants .tscn. Fix : `call_deferred("add_child", child)`
- **`set_deferred("data", val)` → `_ready()` lit null** — assigner directement (`node.data = val`) AVANT `call_deferred("add_child", node)`. Seul le `add_child` a besoin du deferred
- **`add_child(Area2D)` pendant physics flush = CRASH** — signal handler pendant physics → `Can't change this state while flushing queries`. Fix : `container.call_deferred("add_child", node)`

## Type Inference `:=`

- **`:=` sur Array/Dictionary/ternaire/Node générique = crash parser** — source Variant, élément d'Array non-typé, propriété via `Node` générique → "Cannot infer the type". Toujours `: Type =`
- **`var speed := 200` = int, pas float** — `speed = 200.5` → erreur silencieuse. Toujours `200.0`
- **`Array[T]` vs `Array`** — `get_nodes_in_group()` retourne `Array` non-typé. Fix : `var x: Array = method()` ou `x.assign(method())`
- **Accès membre sur Variant = warning cascade** — Fix : cast `as Player` (si `class_name`), ou `has_method()`, ou accepter le Variant

## Tweens & Pause

- **Tween sur node freed = silent fail ou crash** — `create_tween()` lié au node. Fix : `get_tree().create_tween()` (survit au node) ou kill dans `_exit_tree()`
- **Tween pendant pause = 2 conditions** — `process_mode = PROCESS_MODE_ALWAYS` sur le node ET `tw.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)` sur le tween. Un seul ne suffit pas
- **Time scale compensation player** — `var real_delta := delta / Engine.time_scale if Engine.time_scale > 0.0 else delta`. Ne PAS compenser ennemis/projectiles

## Native Member Shadowing

- **Redéfinition membre natif = Parser Error** — `@export var offset` dans `extends Camera2D` → bloquant. Préfixer (`follow_offset`, `shake_offset`, `camera_zoom`)
- Classes piégeuses : Camera2D (`offset`, `zoom`), Node2D (`position`, `rotation`, `scale`), Control (`size`, `position`), CharacterBody2D (`velocity`), Sprite2D (`texture`, `offset`)

## Resources & References

- **`ResourceLoader.exists()` avant `load()` dynamique** — `load("res://x/%s.tres" % type)` crashe si fichier absent. `preload()` vérifié au compile time, `load()` non
- **StyleBoxFlat = référence partagée** — `get_theme_stylebox("panel")` retourne une ref partagée. `.duplicate()` avant modification
- **`duplicate(true)` pour deep copy** — passer des données à l'UI → `weapons.duplicate(true)`, jamais la référence directe

## Architecture & UI

- **Données UI = copies** — l'UI est read-only, communique via signal
- **Action joueur échouée = signal, pas silence** — `return false` sans feedback = bug UX
- **Popup programmatique > .tscn** — CanvasLayer.new() + set_script() + `_build_ui()` pour UI temporaire
- **CanvasLayer ordering** — 0 (jeu), 1 (HUD), 5 (popup gameplay), 6 (popup secondaire), 10 (pause menu)
- **AudioServer effects = index en `_ready()`** — guard clause `bus_idx == -1`, ne jamais recalculer
- **Modulate = fake desaturation** — `Color(0.7, 0.7, 0.7)` sur parent entities = visuel grisé sans shader
- **Temporary nodes > repositioning** — N instances simultanées → nodes temporaires + tween + `queue_free`

## Performance

- **`queue_redraw()` chaque frame = HIGH** — throttle `Engine.get_process_frames() % 3 == 0` ou Sprite+tween
- **`create_timer()` par node = allocation** — utiliser compteur `_time` dans `_process`
- **`get_node()` dans hot paths** — cacher en var `_ready()`

## GPUParticles2D

- **`color_ramp`, `scale_min/max`** = sur `ParticleProcessMaterial`, PAS sur `GPUParticles2D`
- **Sans `draw_pass` = invisible** — toujours ajouter QuadMesh comme `draw_pass_1`
