# GDScript 4.x — Pièges rencontrés

> Référence définitive. Chaque section = un piège avec symptôme, cause et fix.
> Compilé à partir des sessions Column Raid (multi-agent & solo).

---

## Quick-scan checklist (vérifier AVANT de coder)

> TL;DR — si tu ne lis qu'une chose dans ce fichier, lis ça.

- [ ] `var X :=` — source non-triviale ? → `: Type =`
- [ ] `global_position =` — après `add_child()` ?
- [ ] `add_child` dans callback Area2D/Body2D → `call_deferred`
- [ ] Ordre d'émission signaux quand un flag est impliqué
- [ ] `get_theme_stylebox()` sans `.duplicate()` → ref partagée
- [ ] `create_tween()` dans popup pausé → `TWEEN_PAUSE_PROCESS` ?
- [ ] `Engine.time_scale` dans `_physics_process` → compensé player ?
- [ ] Callback signal sur node optionnel → `is_instance_valid()` ?
- [ ] Données passées à l'UI → `duplicate(true)` ?
- [ ] `load()` dynamique (path variable) → `ResourceLoader.exists()` avant ?
- [ ] `await` dans la fonction → `is_instance_valid()` sur TOUS les refs après ?
- [ ] `add_child()` dans `_ready()` parent → `call_deferred` nécessaire ?
- [ ] Propriété GPU particles → sur le Material, pas le node ?
- [ ] `Array[T] = method()` → la source retourne bien `Array[T]` ?
- [ ] `queue_redraw()` dans `_process()` → throttlé ? `get_node()` → caché ?

---

## 1. `:=` — L'inférence de type qui casse tout

**Le plus gros piège rencontré.** 29 fixes sur 12 fichiers en une session.

### Symptôme
```
Parser Error: Cannot infer the type of "X" variable because the value doesn't have a set type.
```
Le jeu **ne démarre pas**. Pas un warning — un Parser Error bloquant.

### Cause
`:=` demande au parser d'inférer le type à la compilation. Si l'expression retourne `Variant` (type dynamique), le parser refuse.

### Expressions qui retournent Variant

| Expression | Retourne | Exemple |
|-----------|----------|---------|
| `dict[key]` | `Variant` | `var cfg := enemies["boss"]` |
| `dict.get(key, default)` | `Variant` | `var hp := data.get("hp", 0)` |
| `.duplicate()` | `Variant` | `var copy := original.duplicate()` |
| `.instantiate()` | `Node` non typé | `var b := scene.instantiate()` |
| `get_node_or_null()` | `Node` nullable | `var p := get_node_or_null("Player")` |
| `load()` sans cast | `Resource` | `var tex := load(path)` |
| `==`, `!=`, `<`, `>` | `Variant` (bool) | `var ok := a == b` |
| `x if cond else y` | `Variant` | `var s := A if flag else B` |
| `get_nodes_in_group()` | `Array` untyped | `var e := get_tree().get_nodes_in_group("x")` |
| `{}` ou `[]` littéral | `Variant` | `var cache := {}` |
| `array[-1]`, `array[i]` | `Variant` (si Array non-typé) | `var last := curve[-1]` |
| `node.property` via Node générique | `Variant` | `var val := generic_node.speed` |
| Arithmétique sur propriétés dynamiques | `Variant` | `var diff := sys.a - sys.b` |

### Expressions SAFE avec `:=`

| Expression | Pourquoi | Exemple |
|-----------|----------|---------|
| Littéraux primitifs | Type évident | `var x := 5`, `var s := "text"` |
| Constructeurs typés | Retour explicite | `var v := Vector2(1, 0)` |
| `SomeClass.new()` | Type connu | `var t := Timer.new()` |
| `preload()` | Retour typé | `var sc := preload("res://x.tscn")` |
| `create_tween()` | Retourne `Tween` | `var tw := create_tween()` |
| `randf_range()`, `randi_range()` | Retournent `float`/`int` | `var r := randf_range(0, 1)` |
| `str()`, `int()`, `float()` | Casts explicites | `var n := int(x)` |
| `minf()`, `maxf()`, `clampf()` | Retournent `float` | `var c := clampf(v, 0, 1)` |
| `absi()`, `absf()` | Retournent `int`/`float` | `var d := absi(a - b)` |
| Accès à un member typé | Annotation dans la classe | `var s := GameState.current_score` |
| `as Type` cast | Force le type | `var sb := bar.get("x") as StyleBoxFlat` |
| `const` declarations | Toujours OK | `const SPEED := 300.0` |

### Fix
```gdscript
# MAUVAIS
var choices := pool.get_random_choices(level)
var config := _enemy_types[type].duplicate()

# BON
var choices: Array = pool.get_random_choices(level)
var config: Dictionary = _enemy_types[type].duplicate()
```

**Règle** : si la source n'est pas un littéral, un constructeur typé ou une fonction à retour garanti → `: Type =` au lieu de `:=`.

### Piège bonus : `:=` via Node générique (cas 3)
```gdscript
var sys: Node = scene.get_node_or_null("ComboSystem")
# MAUVAIS — sys est Node, .combo_window est résolu dynamiquement
var bonus := sys.combo_window - sys.BASE_COMBO_WINDOW

# BON — type explicite
var bonus: float = sys.combo_window - sys.BASE_COMBO_WINDOW
```
Le pattern `var sys: Node = get_node(...)` est fréquent (on ne veut pas preload le script pour typer). Toute opération sur `sys.property` est dynamique → `:=` impossible.

### Piège bonus : int vs float
`var speed := 200` infère `int`. `speed = 200.5` → erreur de type silencieuse. Toujours écrire `200.0` pour les floats.

---

## 2. Ternaire + `:=` — double peine

### Symptôme
```
Parser Error: Cannot infer the type of "speed" variable
```

### Cause
L'expression ternaire `A if condition else B` retourne `Variant` **même si A et B sont du même type**.

```gdscript
# MAUVAIS — ternaire retourne Variant
var speed := SAME_COL_SPEED if same_col else ADJ_SPEED
var sfx := "shoot_alt" if rapid else "shoot"
var color := Color.RED if hp < 25 else Color.GREEN

# BON
var speed: float = SAME_COL_SPEED if same_col else ADJ_SPEED
var sfx: String = "shoot_alt" if rapid else "shoot"
var color: Color = Color.RED if hp < 25 else Color.GREEN
```

Piégeux parce que les deux branches sont souvent du même type — on s'attend à ce que GDScript infère. Mais non.

---

## 3. `global_position` avant `add_child()` — assignation ignorée

### Symptôme
Le node spawne à `(0, 0)` au lieu de la position voulue.

### Cause
`global_position` dépend du parent dans l'arbre. Avant `add_child()`, le node n'a pas de parent → l'assignation est silencieusement ignorée ou écrasée.

### Fix — deux stratégies

**Stratégie A : assigner APRÈS add_child()**
```gdscript
var fx: Node = scene.instantiate()
container.add_child(fx)
fx.global_position = target_pos  # Maintenant il a un parent
```

**Stratégie B : stocker et appliquer dans _ready()**
```gdscript
# Producteur
var fx := Node2D.new()
fx.set_script(preload("res://pickup_fx.gd"))
fx.setup(target_pos, color)
container.add_child(fx)

# pickup_fx.gd
var _spawn_pos := Vector2.ZERO

func setup(pos: Vector2, color: Color) -> void:
    _spawn_pos = pos

func _ready() -> void:
    global_position = _spawn_pos
```

**Attention** : `position` (locale) fonctionne avant `add_child()` si le parent est à `(0,0)`. Seul `global_position` a ce piège.

---

## 4. `add_child()` pendant le physics flush — crash

### Symptôme
```
ERROR: Can't change this state while flushing queries.
```

### Cause
Godot interdit de modifier l'arbre pendant que le moteur physique itère (collisions, `body_entered`/`area_entered`).

### Fix
```gdscript
# MAUVAIS — dans un callback physique
func _on_body_entered(body):
    var fx = fx_scene.instantiate()
    container.add_child(fx)  # CRASH

# BON
func _on_body_entered(body):
    var fx = fx_scene.instantiate()
    container.call_deferred("add_child", fx)
```

### Quand utiliser `call_deferred`
- `add_child()` dans un callback Area2D/Body2D
- `queue_free()` dans un callback physique
- Toute modification de l'arbre dans `_physics_process` si déclenchée indirectement par le moteur

---

## 5. Signaux synchrones — ordre d'émission = comportement

### Symptôme
Les handlers de signaux ne font pas ce qu'on attend — un flag n'est pas encore set, ou est déjà reset.

### Cause
Les signaux GDScript sont **synchrones**. `emit()` exécute tous les handlers immédiatement, dans l'ordre de connexion, **avant** de retourner.

```gdscript
print("before")
EventBus.loot_collected.emit("xp", 10)  # Tous les handlers ICI
print("after")  # APRÈS tous les handlers
```

Si deux signaux sont émis l'un après l'autre, et qu'un handler du premier set un flag vérifié par un handler du second — **l'ordre d'émission détermine le comportement**.

### Fix — suppress flag avec deferred reset
```gdscript
func _on_pile_collected(column: int, items: Array) -> void:
    _suppress_individual_fx = true
    _spawn_pickup_fx(dominant_cat, true)
    call_deferred("_reset_suppress_fx")  # Reset APRÈS le frame

func _on_loot_collected(category: String, _value: int) -> void:
    if _suppress_individual_fx:
        return
    _spawn_pickup_fx(category, false)

func _reset_suppress_fx() -> void:
    _suppress_individual_fx = false
```

---

## 6. Tween sur un node freed — silent fail ou crash

### Symptôme
Le tween ne s'exécute pas, ou crash avec `ObjectDB::instance not valid`.

### Cause
`create_tween()` est lié au node qui l'a créé. Si le node est `queue_free()` avant la fin du tween, le tween est invalidé.

### Fix
```gdscript
# Tween qui survit au node — lié à l'arbre, pas au node
func _die() -> void:
    var tw := get_tree().create_tween()
    tw.tween_property(self, "modulate:a", 0.0, 0.3)
    tw.tween_callback(queue_free)  # Free APRÈS le tween

# OU — kill le tween manuellement
func _exit_tree() -> void:
    if _active_tween and _active_tween.is_valid():
        _active_tween.kill()
```

### Piège subtil
Tween `scale → 1.3 → 1.0` sur un slot HUD, changement de scène pendant l'animation → crash silencieux. Pas bloquant mais les logs s'accumulent.

---

## 7. `queue_free()` n'est pas immédiat

### Symptôme
Un node est `queue_free()` mais il répond encore à un signal, une collision, ou apparaît dans `get_children()`.

### Cause
`queue_free()` marque le node pour suppression **à la fin du frame**. Pendant le frame en cours, le node existe et reçoit signaux/callbacks.

### Fix
```gdscript
# MAUVAIS — le node freed peut trigger ça
func _on_area_entered(area):
    take_damage(10)

# BON — vérifier la validité
func _on_area_entered(area):
    if not is_instance_valid(area) or area.is_queued_for_deletion():
        return
    take_damage(10)
```

---

## 8. Accès membre sur Variant — warning cascade

### Symptôme
```
W 0:00:01 SCRIPT WARNING: The member "current_column" is not declared in the current class
```
Le code fonctionne, mais les warnings polluent la console.

### Cause
Node typé `Node` ou `Node2D` au lieu de son vrai type → GDScript ne connaît pas les membres custom.

### Fix
```gdscript
# Option 1 : class_name disponible
var player: Player = get_node("Player") as Player

# Option 2 : sans class_name
if player.has_method("get_current_column"):
    var col: int = player.get_current_column()

# Option 3 : accepter le Variant (pragmatique pour prototypage)
var player = get_node("Player")  # Pas de type = pas de warning
```

---

## 9. `set_script()` avant `add_child()` = `@onready` OK

Le script attaché via `set_script()` AVANT `add_child()` fonctionne normalement : `@onready` s'initialise, `_ready()` est appelé après.

```gdscript
var popup = CanvasLayer.new()
popup.set_script(preload("res://ui/levelup_popup.gd"))
add_child(popup)  # @onready + _ready() fonctionnent
```

Utile pour les variants de projectiles qui partagent la même structure visuelle (même .tscn, script différent).

---

## 10. StyleBoxFlat — référence partagée silencieuse

### Symptôme
Modifier le style d'une carte/bouton modifie **toutes** les cartes/boutons en même temps.

### Cause
`get_theme_stylebox("panel")` retourne une **référence** à l'instance partagée, pas une copie.

```gdscript
# MAUVAIS — modifie le style de TOUS les PanelContainer
var style: StyleBoxFlat = card.get_theme_stylebox("panel")
style.border_color = Color.GOLD  # Toutes les cartes deviennent gold

# BON — copie avant modification
var style: StyleBoxFlat = card.get_theme_stylebox("panel").duplicate()
style.border_color = Color.GOLD
card.add_theme_stylebox_override("panel", style)
```

### Règle
**Toujours `.duplicate()` avant de modifier un StyleBox récupéré par `get_theme_stylebox()`.** Même si tu n'as qu'un seul node — le style est partagé avec le Theme global.

---

## 11. Tween pendant pause — deux conditions requises

### Symptôme
Le popup pause le jeu, mais l'animation d'entrée ne joue pas.

### Cause
Un tween pendant `get_tree().paused = true` exige **les deux conditions** :
1. Le **node créateur** a `process_mode = PROCESS_MODE_ALWAYS`
2. Le **tween** a `set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)`

Si l'un des deux manque, le tween est gelé.

```gdscript
# MAUVAIS — manque le pause mode sur le tween
var tw := create_tween()
tw.tween_property(_panel, "scale", Vector2.ONE, 0.2)

# BON
var tw := create_tween()
tw.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)
tw.tween_property(_panel, "scale", Vector2.ONE, 0.2)
```

### Piège subtil
`process_mode = ALWAYS` sur un CanvasLayer propage aux enfants. Mais si un enfant a un `process_mode` explicite différent → le tween suit le mode de son créateur, pas celui du parent.

---

## 12. Time scale compensation — bullet time player

### Symptôme
`Engine.time_scale = 0.2` ralentit le joueur alors qu'il devrait bouger à vitesse normale.

### Cause
Godot passe `delta * Engine.time_scale` dans `_physics_process`. Le player reçoit un delta réduit.

### Fix
```gdscript
func _physics_process(delta: float) -> void:
    var real_delta := delta / Engine.time_scale if Engine.time_scale > 0.0 else delta
    # Utiliser real_delta pour TOUT le mouvement et le juice du player
    position.x += _speed_x * real_delta
```

### Ce qu'il NE faut PAS compenser
- Ennemis, projectiles, loot : ils DOIVENT être ralentis (c'est le but)
- Tweens (`create_tween()`) : respectent déjà le time_scale
- Timer nodes : respectent déjà le time_scale

### Guard clause
`Engine.time_scale > 0.0` — si time_scale est 0 (freeze total), division par zéro. Utiliser delta tel quel.

---

## 13. `is_instance_valid()` dans les callbacks signal

### Symptôme
Crash `ObjectDB::instance not valid` quand un signal arrive après `queue_free()` d'un node référencé.

### Cause
Les signaux s'exécutent synchroniquement. Un callback peut référencer un node optionnel (overlay, vignette, FX) qui a été freed entre-temps.

```gdscript
# MAUVAIS — crash si _bt_vignette est freed
func _on_bullet_time_started(_ts: float) -> void:
    var tw := create_tween()
    tw.tween_property(_bt_vignette, "color:a", 0.35, 0.15)

# BON
func _on_bullet_time_started(_ts: float) -> void:
    if not is_instance_valid(_bt_vignette):
        return
    var tw := create_tween()
    tw.tween_property(_bt_vignette, "color:a", 0.35, 0.15)
```

### Règle
**Tout callback de signal qui manipule un node optionnel → `is_instance_valid()` en premier.** Les nodes UI, FX, overlays sont particulièrement à risque (scene change, queue_free asynchrone).

---

## 14. Données snapshot vs référence — `duplicate(true)` pour l'UI

### Symptôme
Le popup modifie accidentellement les données du système logique (WeaponManager, Inventory, etc.).

### Cause
Passer un `Array[Dictionary]` à l'UI donne une **référence**. `duplicate()` (shallow) copie l'array mais les dicts internes restent partagés.

```gdscript
# MAUVAIS — shallow copy, les dicts sont des refs
var snapshot: Array = weapons.duplicate()
popup.show(snapshot)
# popup modifie snapshot[0]["level"] → change weapons[0]["level"] aussi

# BON — deep copy
func get_weapons_snapshot() -> Array[Dictionary]:
    return weapons.duplicate(true)
```

### Règle
**Toute donnée passée du système logique à l'UI = `.duplicate(true)` (deep copy).** L'UI est read-only. Elle affiche et communique le résultat via signal, jamais de modification directe.

---

## 15. `ResourceLoader.exists()` avant `load()` dynamique

### Symptôme
```
Invalid resource path: res://entities/enemies/explosive_01_frames.tres
```
Crash en cascade : load échoue → variable null → méthode sur null.

### Cause
`load()` avec un path construit dynamiquement (`"res://x/%s.tres" % type`) crashe si le fichier n'existe pas. `preload()` est vérifié au compile time, mais `load()` dynamique ne l'est pas.

### Fix
```gdscript
var frames_path := "res://entities/enemies/%s_frames.tres" % enemy_type
if not ResourceLoader.exists(frames_path):
    frames_path = "res://entities/enemies/default_frames.tres"
sprite.sprite_frames = load(frames_path)
```

### Règle
**Toujours `ResourceLoader.exists()` avant `load()` quand le path est variable.** Pour les paths statiques, préférer `preload()`.

---

## 16. `await` + objet freed — guard après chaque await

### Symptôme
```
Invalid access to property on a previously freed object.
```

### Cause
Après un `await`, n'importe quel node référencé peut avoir été `queue_free()` pendant l'attente. Pas seulement `self` — la source, la cible, le container.

### Fix
```gdscript
await get_tree().create_timer(0.08).timeout

if not is_instance_valid(self):
    return
if not is_instance_valid(from_enemy):
    queue_free()
    return
```

### Checklist post-await
1. `is_instance_valid(self)` — le node courant existe encore ?
2. `is_instance_valid(target)` — la cible existe encore ?
3. `is_instance_valid(container)` — le parent/container existe encore ?

**Règle** : après CHAQUE `await`, vérifier `is_instance_valid()` sur TOUS les objets référencés avant le await.

---

## 17. `add_child()` pendant `_ready()` du parent — call_deferred

### Symptôme
```
Parent node is busy setting up children, `add_child()` failed. Consider using `add_child.call_deferred(child)` instead.
```

### Cause
Quand `_ready()` d'un node s'exécute, le parent est encore en train de setup ses enfants définis dans le .tscn. `add_child()` direct échoue.

### Fix
```gdscript
func _ready() -> void:
    var lbl := Label.new()
    # MAUVAIS — si le VBox parent est en cours de setup
    vbox.add_child(lbl)

    # BON
    vbox.call_deferred("add_child", lbl)
```

### Attention
`call_deferred` = les enfants ne seront pas disponibles dans le même frame. Si tu dois accéder aux enfants immédiatement, utilise `add_child()` direct sur un node pas encore dans le tree (ex: children d'un row pas encore ajouté).

---

## 18. GPUParticles2D vs ParticleProcessMaterial — propriétés confondues

### Symptôme
```
Invalid assignment of property or key 'color_ramp' with value of type 'GradientTexture1D' on a base object of type 'GPUParticles2D'.
```

### Cause
Dans les fichiers `.tscn`, `color_ramp` apparaît comme propriété du node GPUParticles2D. Mais en GDScript, c'est une propriété de `ParticleProcessMaterial`, pas du node.

```gdscript
# MAUVAIS
particles.color_ramp = grad_tex

# BON
mat.color_ramp = grad_tex
particles.process_material = mat
```

### Table de confusion courante

| Propriété | Vit sur | PAS sur |
|-----------|---------|---------|
| `color_ramp` | `ParticleProcessMaterial` | `GPUParticles2D` |
| `color` | `ParticleProcessMaterial` | `GPUParticles2D` |
| `scale_min/max` | `ParticleProcessMaterial` | `GPUParticles2D` |
| `amount` | `GPUParticles2D` | `ParticleProcessMaterial` |
| `lifetime` | `GPUParticles2D` | `ParticleProcessMaterial` |
| `one_shot` | `GPUParticles2D` | `ParticleProcessMaterial` |
| `explosiveness` | `GPUParticles2D` | `ParticleProcessMaterial` |

---

## 19. `Array[T]` vs `Array` — assignation incompatible

### Symptôme
```
Trying to assign an array of type "Array" to a variable of type "Array[Node]"
```

### Cause
Beaucoup de méthodes Godot retournent `Array` non-typé, pas `Array[T]`. GDScript refuse l'assignation implicite.

```gdscript
# MAUVAIS
var enemies: Array[Node] = get_tree().get_nodes_in_group("enemies")

# BON — type non-contraint
var enemies: Array = get_tree().get_nodes_in_group("enemies")

# BON — conversion explicite
var enemies: Array[Node] = []
enemies.assign(get_tree().get_nodes_in_group("enemies"))
```

### Méthodes retournant Array non-typé (piège)

| Méthode | Retourne | PAS |
|---------|----------|-----|
| `get_nodes_in_group()` | `Array` | `Array[Node]` |
| `get_connections()` | `Array` | `Array[Dictionary]` |
| `get_groups()` | `Array` | `Array[StringName]` |
| `get_children()` | `Array[Node]` | — (celui-là EST typé) |

---

## 20. Performance — anti-patterns courants en GDScript

### `queue_redraw()` chaque frame (HIGH)
```gdscript
# MAUVAIS — 60 draw calls/sec par node avec _draw()
func _process(delta: float) -> void:
    _time += delta
    queue_redraw()

# BON — throttle ~20fps, visuellement identique
func _process(delta: float) -> void:
    _time += delta
    if Engine.get_process_frames() % 3 == 0:
        queue_redraw()
```
Alternative : remplacer `_draw()` par un Sprite + tween = zéro draw calls.

### `create_timer()` pour auto-destruction (HIGH)
```gdscript
# MAUVAIS — alloue un Timer par node (dizaines en même temps)
get_tree().create_timer(LIFETIME).timeout.connect(queue_free)

# BON — utiliser le compteur déjà dans _process
func _process(delta: float) -> void:
    _time += delta
    if _time >= LIFETIME:
        queue_free()
        return
```

### `get_node()` non-caché dans les hot paths (MEDIUM)
```gdscript
# MAUVAIS — lookup à chaque tir (3x/sec × 4 armes = 12 lookups/sec)
func _shoot() -> void:
    var sprite: AnimatedSprite2D = _player.get_node_or_null("Sprite")

# BON — caché dans _ready()
var _player_sprite: AnimatedSprite2D

func _ready() -> void:
    _player_sprite = _player.get_node_or_null("Sprite") as AnimatedSprite2D
```

### Checklist perf rapide

| Anti-pattern | Sévérité | Fix |
|-------------|----------|-----|
| `queue_redraw()` dans `_process()` | HIGH | Throttle `% 3` ou Sprite+tween |
| `create_timer()` pour self-destruct | HIGH | Compteur `_time` dans _process |
| `get_node()` dans callbacks fréquents | MEDIUM | Cacher en var dans `_ready()` |
| `get_children()` dans _process | MEDIUM | Cacher, invalider sur child changed |
| Tween recréé chaque frame | MEDIUM-HIGH | Garder ref, kill+recréer si état change |
| `load()` non-preload dans hot path | HIGH | `preload()` ou cacher le résultat |

---

## Bilan — règles à appliquer systématiquement

### Grep checks avant chaque session
```
[ ] var \w+ :=     — vérifier chaque source non-triviale
[ ] global_position =  — vérifier que c'est après add_child
[ ] add_child dans callbacks Area2D/Body2D — utiliser call_deferred
[ ] Ordre d'émission des signaux quand un flag est impliqué
[ ] get_theme_stylebox sans .duplicate() — référence partagée
[ ] create_tween() dans un popup pausé — TWEEN_PAUSE_PROCESS ?
[ ] Engine.time_scale dans _physics_process — compensé pour le player ?
[ ] Callbacks signal sur nodes optionnels — is_instance_valid() ?
[ ] Données passées à l'UI — duplicate(true) ?
```

### Table récap

| Situation | Faire | Ne pas faire |
|-----------|-------|-------------|
| Assigner depuis dict/duplicate/instantiate | `: Type =` | `:=` |
| Comparer avec `==` et stocker | `: bool =` | `:=` |
| Ternaire | `: Type =` | `:=` |
| `global_position` sur un node frais | Après `add_child()` ou dans `_ready()` | Avant `add_child()` |
| `add_child` dans callback physique | `call_deferred("add_child", node)` | `add_child(node)` |
| Tween sur un node qui va mourir | `get_tree().create_tween()` | `create_tween()` |
| Utiliser un node après `queue_free()` | `is_instance_valid()` check | Accès direct |
| Signaux synchrones + flag | `call_deferred` pour reset | Reset immédiat |
| StyleBox depuis theme | `.duplicate()` avant modification | Modifier la référence directe |
| Tween pendant pause | `TWEEN_PAUSE_PROCESS` + `PROCESS_MODE_ALWAYS` | `create_tween()` seul |
| Player speed pendant bullet time | `delta / Engine.time_scale` | `delta` brut |
| Node optionnel dans callback signal | `is_instance_valid()` check | Accès direct |
| Données passées à l'UI | `.duplicate(true)` (deep copy) | Référence directe |
| `load()` avec path variable | `ResourceLoader.exists()` avant | `load()` direct |
| Code après `await` | `is_instance_valid()` sur TOUS les refs | Accès direct |
| `add_child()` dans `_ready()` parent | `call_deferred("add_child", node)` | `add_child(node)` |
| Propriété particules (color_ramp, scale) | Sur `ParticleProcessMaterial` | Sur `GPUParticles2D` |
| `Array[T] = untyped_method()` | `Array` brut ou `.assign()` | Assignation directe |
| `:=` via `Node` générique | `: Type =` explicite | `:=` (Variant dynamique) |
| `queue_redraw()` dans `_process` | Throttle `% 3` ou Sprite | Chaque frame |
| `get_node()` fréquent | Cacher en var `_ready()` | Lookup répété |
