# Relais — animations de retour de plongée

Rédigé le 23/09/2026, implémenté le même jour par Codex.
Les sections numérotées ci-dessous conservent l'enquête et le plan d'origine.

## État livré — version procédurale

Les trois retours fonctionnent dans `scripts/core/restaurant_return_3d.gd` :
marche depuis la droite, marche épuisée avec trois respirations, et deux
ambulanciers avec civière, coup de pied, vol, impact, glissade et relevé en
trois tentatives. Entrée ou Retour (actions `ui_accept` / `ui_cancel` : B,
Retour arrière, clic droit) passent la séquence après 0,4 seconde. Échap pose
la pause par-dessus, qui fige la séquence (`docs/mapping-touches.md`).

- Les quatre fins de plongée posent le motif dans `GameState.pending_return`.
  Il est consommé une fois, absent des sauvegardes et effacé au chargement/reset.
- Le restaurant attend la physique et l'avatar, puis choisit une position libre
  autour du centre avec la vraie forme et le masque de collision du joueur.
- `return_active` suspend le pont. La porte s'ouvre visuellement sans changer
  le rideau ni l'enseigne du restaurant; son état final suit `dining_is_open()`.
- Simulation, HUD, badges, animation, caméra et contrôles sont restaurés en
  fin normale, au passage rapide et à la destruction du module.
- Le cadrage du K.-O. réserve de la place à droite pour rendre le coup de pied
  visible. Poses couchée, coup de pied, vol et relevé inspectées par captures
  rendues dans `_backups/return-review/`.
- Les quatre sons `return_*` sont déclarés et configurés dans maître ET
  brouillon, validés avec les fonctions PHP du gestionnaire. Les anciens types
  numériques et objets vides ont été conservés : seuls ces sons et la date
  de modification changent dans les données.

**Limite artistique :** aucun nouvel asset Meshy/Blender n'a été généré. Les
ambulanciers sont en primitives, les poses utilisent les clips du héros et des
transformations procédurales. Le souffle et le grognement réutilisent des sons
existants; ils sont remplaçables dans le groupe « Retour de plongée » du
gestionnaire. L'import des clips Meshy envisagé au §4 reste une extension.

### Preuves de validation

`python tools/dev/dev.py check --area return` lance le contrôle dédié, également
inclus dans `--area flow`. Il éprouve les vrais changements de scène et les
quatre sorties du donjon, les collisions au centre, les portes fermée/ouverte,
les comptes audio, les contrôles, le passage rapide et l'interruption.

- Retour, parcours réel et couverture des émetteurs audio : **PASS**,
  `tools/dev/runs/20260923-123803-checks-5_go0q17/report.json`.
- Pont 3D, audio, audit des sons configurés, éditeur audio navigateur,
  validation PHP audio et outils : **PASS**, sous-contrôles correspondants de
  `tools/dev/runs/20260923-123654-checks-os55lqr9/report.json`.
  Les deux échecs initiaux de ce rapport (ancien parcours instantané et
  ancienne exception d'émetteur de porte) ont été corrigés et revalidés ci-dessus.
- Rouge/vert : troisième respiration retirée volontairement, échec ciblé
  dans `20260923-123634-checks-n9d3y98d`, puis restauration et PASS.
- Capture rendue avec sauvegarde isolée : **PASS**, journal
  `tools/dev/runs/20260923-123557-return-capture-3tkilbnm/output.log`.
- Gameplay : **FAIL**, 44 assertions / 47 entrées du runner, reproduit dans
  `20260923-123834-checks-vp5vcnhb` et `20260923-124031-checks-s_56tj8a`.
  Avant chantier : 43 assertions / 46 entrées (`20260923-122252-checks-mfm4xyll`).
  Deux assertions variables de télégraphe/recul remplacent une assertion de
  cône; toutes sont antérieures (télégraphe/recul retrouvés dans
  `20260922-205946-checks-j5kh6x2_/gameplay/output.log`). Aucun nouvel échec
  identifié. Les six contrôles annexes du gameplay passent.
  Comparaison manuelle des identités : le runner refuse la comparaison
  automatique parce que `checks.py` a acquis la zone `return`; Godot, graine,
  fixture et sélection gameplay sont identiques.

Voir aussi `docs/current-task.md` pour le checkpoint de cette reprise.

## 1. La demande de l'auteur (verbatim)

> jaimerais que le retour de plongé se fasse avec une animation speciale selon la
> situation, retour volontaire le heros reviens en marchant tranquilement
> normalement depuis la droite la porte ouvre et se referme si necessaire et le
> heros marche jusqua millieu du restaurant. quand cest par manque de stamina, le
> heros reviens de peine et misere sufffoquant, mais utilisant le meme pattern,
> arrive au milieu du restaurant prend trois grandes respirations dificille, puis
> quand on est knockout le heros arrive sur une civierres tenue par 2 ambulancier
> il se placent devant la porte et kickent le heros a linterieur de son
> restaurant qui atterie durement par terre, puis se releve de peine et de
> misere, utilise meshy et/ou blender mcp pour faire ce que tu as a faire

Trois séquences :

| Cas | Déclencheur | Séquence |
|---|---|---|
| `walk` | départ volontaire, victoire de boss | entre à pied depuis la droite (hors écran), porte est s'ouvre puis se referme si la salle est fermée, marche jusqu'au milieu |
| `exhausted` | stamina à zéro | même trajet, titubant et suffocant, puis **trois** respirations difficiles au milieu |
| `knocked_out` | PV à zéro | deux ambulanciers portent le héros sur une civière, s'arrêtent devant la porte, le **bottent** à l'intérieur; atterrissage brutal, puis le héros se relève péniblement |

## 2. Blocages connus

- **Pas de Blender MCP** dans l'environnement.
- **Meshy bloqué** : le classificateur de sécurité refuse toute commande où la
  clé figure en clair (`MESHY_API_KEY=msy_... node session.mjs`), à juste titre.
  L'auteur doit d'abord faire `setx MESHY_API_KEY "<nouvelle clé>"`; l'ancienne
  clé a été exposée dans la conversation et doit être **régénérée**. Ensuite, lire
  la clé sans l'écrire, par exemple :
  `MESHY_API_KEY="$(powershell.exe -NoProfile -Command "[Environment]::GetEnvironmentVariable('MESHY_API_KEY','User')" | tr -d '\r')" node tools/meshy/session.mjs`.
  Ne jamais écrire la clé dans un fichier du projet.
- **Recommandation** : livrer d'abord une version entièrement procédurale, qui
  marche sans Meshy (clips existants et ambulanciers en primitives), avec des
  emplacements facultatifs pour les clips Meshy. Les ajouter ensuite, dès que la
  clé est disponible.

## 3. Où brancher — faits vérifiés

### Fins de plongée — `scripts/combat/dungeon.gd`
- `_complete_dimension_flow()` (~l.690) : victoire de boss → `walk`.
  Le changement de scène est direct, à la ligne ~717.
- `_update_objective()`, branche `action == "restaurant"` (~l.755) → `walk`.
- `_end_exhausted_run()` (~l.808) → `exhausted`. Ignoré en bac à sable.
- `_fail_run()` (~l.828) → `knocked_out`. Ignoré en bac à sable.
- Les trois derniers passent par `dive_result.present(...)`, puis
  `_on_dive_result_closed()` change de scène vers le restaurant.
- Poser le motif **avant** `end_run()`/`present`, comme `pending_toast`.

### Transport du motif — `autoload/game_state.gd`
- Modèle à copier : `pending_toast`, `set_pending_toast`,
  `consume_pending_toast` (~l.46, ~l.624).
- Ajouter `pending_return := ""`, les constantes
  `RETURN_WALK/EXHAUSTED/KNOCKED_OUT`, un setter et un `consume_pending_return()`.
  Le remettre à vide là où `pending_toast = ""` est réinitialisé (~l.2819).
- Mémoire seulement : **ne pas persister**. Un rechargement de sauvegarde ne
  rejoue rien.

### Restaurant — `scripts/restaurant/restaurant.gd`
- `_ready()` (~l.340). À la fin, lire `GameState.consume_pending_return()` et,
  si le motif n'est pas vide, lancer une coroutine `_play_return(kind)`.
  Pendant la séquence : `transitioning = true`, `player.controls_enabled = false`.
- **Modèle exact à imiter** : le départ, dans `handle_interaction("dungeon")`
  (~l.1349). Il crée `DungeonDeparture.new()`, l'ajoute à
  `Bridge3D.world_root()`, appelle `begin(self)`, attend `finished`, puis
  `queue_free()`.
- L'avatar 3D du héros n'existe peut-être pas encore au `_ready` : attendre
  quelques images jusqu'à ce que `Bridge3D._avatars.get(player)` soit valide.
  Au-delà de ~10 images, abandonner en silence : le joueur apparaît simplement.
- Géométrie : `ROOM_RECT := Rect2(510, 200, 1040, 652.5)`, `STREET_SIDE := "east"`,
  `street_door()` = y ≈ 617.6 (64 % de la façade), `street_line()` = x du trottoir
  (`ROOM_RECT.end.x + 98`), `DOOR_WIDTH 200`, `entrance_point()`, et
  `east_sidewalk_rect()`, qui descend jusqu'à `ROOM_RECT.end.y + 120`.
- Le joueur naît à `(847, 650)`, d'après `scenes/restaurant/restaurant.tscn`.
- **Point d'arrivée** (« milieu du restaurant ») : il doit être libre pour la
  collision 2D du joueur. Écrire `return_spot()`, qui essaie des candidats
  autour de `ROOM_RECT.get_center()` et le long de `y = street_door()` via
  `get_world_2d().direct_space_state.intersect_shape`. La requête prend la forme
  du `CollisionShape2D` du joueur, son `collision_mask`, et exclut son RID.
  Repli : la position de départ du joueur. Placer `player.global_position` sur
  ce point **avant** la séquence, pour que la reprise du pont 3D ne fasse pas
  sauter l'avatar.
- Porte : le son existant `restaurant_door_open/close` est émis par
  `_set_door_closed()` (~l.3775), mais ne suit que `dining_is_open()`.

### Pont 3D — `autoload/bridge_3d.gd`
- `_process()` (~l.356) : sous `_root.has_meta("departure_active")`, le pont
  **cesse** de synchroniser les avatars (`_sync`). La séquence possède alors les
  transformations. Choisir : réutiliser cette méta, ou la généraliser en
  « cinématique active » (même test aux lignes ~343 et ~376).
- Porte 3D : le nœud `"RestaurantEntrance"` sous `world_root()`, avec les
  vantaux `DoorLeaf0/1`. On la pilote à la main chaque image avec
  `Bridge3D._apply_restaurant_entrance(entrance, opened, false)` (~l.641).
  Attention : `RESTAURANT_DOOR_SPEED 0.12` s'applique **par image**, pas par
  seconde.
- Hors séquence, `_sync_restaurant_entrance` la ramène à `dining_is_open()`.
- `Shape3DFactory.to_3d(Vector2)` donne `Vector3(x, 0, y)` : 1 px = 1 unité.

### Le modèle à imiter — `scripts/core/dungeon_departure_3d.gd` (401 lignes)
Il fournit tout le squelette :
- `Stage.claim(..., Stage.PRIORITY_CEREMONY + 1)` / `Stage.release` (`scripts/core/stage.gd`);
- HUD et badges masqués puis restaurés (`_hide_badges`, `_hidden`);
- `scene.process_mode = DISABLED`, avec le module et le héros en `PAUSABLE`;
- `_input` qui avale les entrées;
- fondu (`_build_overlay`), particules de fumée (`_smoke`, `_update_particles`),
  secousse de caméra (`_impact_age`);
- `_restore()` idempotent, appelé aussi dans `_exit_tree`;
- `AudioDirector.play_event(id, {"emitter": ..., "scene": AUDIO_SCENE})`, puis
  `release_scene`.
- Orientation : `rotation.y = PI*0.5` fait face à +X. Pour marcher vers l'ouest
  (entrée par la droite), prendre `-PI*0.5`. Le modèle regarde vers +Z local :
  pencher **en avant** = `rotation.x` positif; `rotation.z` = roulis latéral.

### Animations du héros
- Config : `player_configs/default_player/model_3d` dans le maître. Le GLB est
  `res://assets/models/player/intro_cook_meshy/intro_cook_meshy.glb`, à l'échelle 1.5.
- Clips existants : `Walking, Running, RunFast, Idle, Hurt, Triple_Combo_Attack,
  Dead, Skill_01`. **Aucun** clip de marche épuisée, de souffle, de chute ou de
  relevé.
- Le héros est piloté par un **AnimationTree** (couche `upper_body` pour
  l'attaque). Pour jouer un clip libre, le plus simple est
  `tree.active = false` puis `AnimationPlayer.play(clip)`. En fin de séquence :
  `tree.active = true`, remettre les métas `animation_state` et
  `animation_requested_state` à `""`, puis
  `Model3DFactory.show_state(hero, "idle", "idle", "", true)`.
  Voir `_show_layered_state` et `_set_tree_base` dans
  `scripts/core/model_3d_factory.gd`, ~l.200.
- Les noms réels des clips peuvent porter un préfixe : les résoudre par suffixe,
  ou via `Model3DFactory._resolve_clip`.

## 4. Plan de mise en œuvre proposé

1. **Point de reprise** : `git commit` avant de commencer (les commits sont
   autorisés; `checkout/reset/restore/stash` sur les données sont **interdits**).
2. `GameState.pending_return`, posé dans les 4 chemins de `dungeon.gd`.
3. Nouveau `scripts/core/restaurant_return_3d.gd` (`extends Node3D`, `signal
   finished`, `begin(scene, kind) -> bool`, `elapsed`, `kind`). Mêmes
   garanties que le départ, avec en plus :
   - **passer la séquence** sur `ui_accept`/`ui_cancel` après ~0,4 s, en sautant
     à l'état final. Le retour a lieu à chaque plongée, il lassera vite;
   - trajet en deux segments : départ hors écran `(ROOM_RECT.end.x + 420,
     street_door())` → seuil intérieur `(ROOM_RECT.end.x - 70, street_door())` →
     `return_spot()`;
   - porte ouverte tant que le héros (ou la civière) est à moins d'environ 170 du
     seuil; refermée ensuite **seulement si** `not dining_is_open()`.
4. **walk** : clip `Walking` à vitesse 1, environ 240 unités/s. À l'arrivée : `Idle`, face à la
   caméra ou vers l'ouest.
5. **exhausted** : départ plus proche (`end.x + 260`), `Walking` à environ 0,55, penché en avant
   (`rotation.x ≈ 0.3`), roulis `sin(t*3)*0.12`, vitesse pulsée, un faux pas à mi-chemin,
   bouffées d'haleine (petites sphères blanches devant la tête). Au milieu :
   **trois** cycles d'environ 1,15 s : inspiration (se redresse, `scale.y` 1.06), expiration
   (se plie, `scale.y` 0.95, bouffée, son).
6. **knocked_out** :
   - civière procédurale : deux brancards, un matelas blanc, poignées;
   - deux ambulanciers procéduraux : jambes et bras sur pivots pour balancer les
     membres et donner le coup de pied, tronc blanc à croix rouge, casquette
     marine, hauteur ≈ `hero.get_meta("tall")`;
   - arrivée par le trottoir est, du **sud** vers la porte, civière parallèle au mur;
   - arrêt devant la porte, civière posée;
   - un ambulancier passe côté est du héros et le botte vers −X. Vol en
     parabole à travers la porte, premier impact vers 70 % du trajet (poussière,
     secousse caméra, son), puis glissade jusqu'à `return_spot()`;
   - le héros reste couché environ 1 s, puis se relève procéduralement : deux tentatives
     ratées à mi-hauteur, puis debout, avec le clip `Hurt`;
   - les ambulanciers repartent, et la porte se referme si nécessaire.
   Pose couchée : essayer la dernière image de `Dead`, sinon `Idle` basculé
   (`Basis(Y, yaw) * Basis(X, -PI/2)` = sur le dos). **Vérifier par capture**
   (voir §6).
7. Sons : ajouter un groupe `"return": "Retour de plongée"` dans
   `data/audio_contract.json`, avec les événements `return_gasp`, `return_kick`,
   `return_land` et `return_groan`.
   - Les valeurs par défaut vont dans `audio_defaults.events` des **deux**
     fichiers de données, via un script PHP qui appelle
     `validate_and_normalize()` + `write_json_file()` (modèle ci-dessous).
   - Clips plausibles : `attack_impact_blunt` (coup de pied),
     `departure_furniture_land` (atterrissage), `enemy_hurt_large` aigu
     (grognement).
   - Aucun clip de souffle n'existe : laisser à l'auteur, ou détourner
     `player_dash_start` grave.
   - Le test `tools/game-data-manager/tests/audio_emitter_coverage_test.js`
     exige un émetteur pour chaque événement déclaré.
8. Une fois la clé Meshy disponible (3 crédits par clip, environ 440 crédits
   restants), appeler `meshy_animate` sur le **rig du héros**
   `01a0aa67-ce2b-70d7-9cec-dd2629be00f8` :
   - 111 `Injured_Walk`, 31 `Catching_Breath` (ou 338 `Short_Breathe_and_Look_Around`);
   - 502–505 `Fall1..4` ou 506–508 `Dive_Down_and_Land*`;
   - 344–353 `Stand_Up1..10`, et 266 `Lie_Down_Hands_Spread` pour la civière.

   Chaque tâche rend un GLB à clip unique sur le même squelette. Les ranger sous
   `assets/models/player/intro_cook_meshy/return/`. Au chargement, en extraire
   l'`Animation` et l'ajouter à une `AnimationLibrary` du lecteur du héros. Les
   chemins de pistes doivent correspondre : le vérifier.

   Ambulancier : `meshy_text_to_3d` (T-pose, low-poly, uniforme d'ambulancier),
   environ 10 crédits pour l'aperçu et 30 pour le raffinage, puis `meshy_rig`,
   puis `meshy_animate` 551 `Carry_Heavy_Object_Walk` + 103 `Simple_Kick`.

   Outil : `tools/meshy/session.mjs`. Il lit des lignes JSON sur stdin : `{}`,
   puis `{"tool":..,"args":..}`, puis `{"close":true}`. Voir
   `tools/meshy/README.md`.

## 5. Validation attendue (niveau 2–3)

- Étendre `scripts/core/bridge_3d_flow_check.gd` sur le modèle de
  `_run_dungeon_departure` (~l.134), ou créer un contrôle dédié enregistré dans
  `tools/dev/checks.py`. Pour chaque motif :
  - la séquence démarre, la scène est gelée, le HUD masqué;
  - la porte s'ouvre pendant le passage, puis revient à `dining_is_open()`;
  - le héros finit sur `return_spot()`, qui est libre en 2D;
  - trois respirations exactement (compter les `return_gasp` via
    `AudioDirector.recorded_events`);
  - un coup de pied, un atterrissage (secousse);
  - tout est restauré : `Stage` relâché, méta retirée, HUD visible, contrôles
    rendus, `tree.active` rétabli;
  - passer la séquence mène au même état final.
- Vérifier aussi que `dungeon.gd` pose le bon motif dans chaque chemin.
- **Rouge/vert une fois** : réintroduire une régression, voir l'échec, restaurer.
- Commandes : `python tools/dev/dev.py check --area flow`, puis `--area visuals`
  et `--area audio`, puis `gameplay`. Pour ce dernier, comparer les **noms**
  d'échecs à la base connue (~45–47 échecs préexistants et plusieurs assertions
  instables : répéter 3 fois avant d'accuser le chantier).
- Côté manager, sept échecs sont déjà connus (voir la mémoire du projet).
- Captures : un run non headless peut écrire
  `get_viewport().get_texture().get_image().save_png(...)`. Le départ le fait
  sous `--departure-capture`, dans `res://_backups/departure-review` : l'imiter
  pour vérifier les poses.

## 6. Garde-fous

- Ne jamais `git checkout/restore/stash/reset` sur `data/game_data_master.json`
  ou `tools/game-data-manager/data/draft.json` : l'auteur publie en parallèle
  depuis le navigateur.
- Des suppressions de `tools/game-data-manager/data/backups/*.json` apparaissent
  dans `git status` : elles viennent du manager, n'y pas toucher.
  `assets/models/restaurant/service_counter/service_counter.glb.import` et
  `service_counter_0.jpg` sont à committer avec le reste : ils accompagnent le
  modèle du comptoir déjà suivi.
- Textes affichés : clés `Loc`. Aucun littéral nouveau.
- Scripts d'édition : les écrire dans le scratchpad avec l'outil Write. Pas de
  heredoc Python (corruption des antislashs). Les fichiers mêlent CRLF et LF :
  `loc.gd` est en CRLF.
- Commit avec la ligne `Co-Authored-By` en vigueur.

### Modèle de script PHP pour les données (déjà éprouvé)

```php
<?php
require_once '<racine>/big-egg-3d/tools/game-data-manager/config.php';
foreach (['<racine>/big-egg-3d/data/game_data_master.json',
          '<racine>/big-egg-3d/tools/game-data-manager/data/draft.json'] as $f) {
    $d = json_decode(file_get_contents($f), true, 512, JSON_THROW_ON_ERROR);
    if (!isset($d['audio_defaults']['events']['return_kick'])) {
        $d['audio_defaults']['events']['return_kick'] = ['mode' => 'custom',
            'variants' => [['clip' => 'attack_impact_blunt', 'weight' => 1, 'volume_db' => -4,
                            'pitch_min' => 0.9, 'pitch_max' => 1.0]],
            'probability' => 1, 'cooldown_seconds' => 0, 'max_instances' => 1, 'avoid_repeat' => false];
    }
    [$n, $e] = validate_and_normalize($d);
    if ($e) { print_r(array_slice($e, 0, 5)); exit(1); }
    write_json_file($f, $n);
}
```
PHP : `/c/wamp64/bin/php/php8.4.0/php.exe -n`.
