# Checkpoint 6A — les phases s'entendent et s'expliquent

22 septembre 2026. Reprise de `f203146`, travail exclusivement dans
`big-egg-3d/`. Premier tiers du lot 6 : **sons de phase et diagnostics**. La
présentation visuelle et le bac à sable suivent en 6B et 6C.

## Ce qui est livré

### Trois sons de phase

Déclarés **une seule fois**, dans `data/audio_contract.json`, que PHP et Godot
lisent tous les deux. Ils apparaissent donc d'eux-mêmes dans le gestionnaire,
sur la fiche d'un monstre, à côté des sons existants.

| Événement | Quand il parle |
| --- | --- |
| `enemy_phase_enter` | Une fois par changement **réel** de phase. |
| `enemy_phase_exit` | La phase quittée parle avant que la suivante entre. |
| `enemy_shield_block` | Un coup **refusé** : bouclier de phase, ou fenêtre d'invulnérabilité d'un geste. |

Trois pièges nommés par le plan sont tenus et éprouvés :

- un **maintien final** ne rejoue pas l'entrée ;
- un **nouveau tour de boucle sur la même phase** ne la rejoue pas non plus —
  l'occurrence de segment change, la créature ne redevient pas autre ;
- un **soin refusé** par la même porte n'est pas un coup bloqué.

Un coup bloqué se voyait déjà (le zéro gris) ; il s'entend maintenant. Sans
bruit, le joueur croit avoir raté son coup plutôt que l'avoir vu bloquer.

### Le diagnostic

`Enemy.encounter_diagnostic()` répond à la question que l'auteur pose vraiment :
**pourquoi cette phase n'arrive pas ?**

Il rend, par ligne : armée ou non, curseur, position dans la boucle, occurrence,
phase proposée, et une **raison stable** parmi `disabled`, `waiting_conditions`,
`arming`, `hole`, `ended`, `masked`, `holding`, `active`. Puis, pour la
transition en attente : `minimum_exposure` (avec le temps restant),
`finishing_attack` (avec le geste qui retient et le plafond restant),
`applying`, ou `settled`.

Deux distinctions comptent et sont testées :

- une ligne **éteinte** ne se dit pas « en attente de condition » : ce sont deux
  corrections différentes à faire dans le gestionnaire ;
- une ligne **masquée** par une ligne supérieure ne se dit pas cassée : elle est
  couverte, et le diagnostic le dit.

Les clés sont stables ; les phrases se fabriquent à l'affichage. Le moteur ne
fabrique pas de texte.

### Le coût

`EncounterDirector.explain()` est **séparé** de `advance()`. Expliquer coûte plus
cher que décider, et personne ne regarde la plupart du temps : le combat ne paie
donc jamais le diagnostic. La console et le bac à sable l'appellent à leur propre
cadence. Deux lectures de suite rendent exactement la même chose — c'est vérifié,
parce qu'un diagnostic qui modifie l'état qu'il observe ne sert à rien.

## Ce que le cablage a mis au jour

`tests/audio_emitter_coverage_test.js` est neuf : il compare les événements
**déclarés** au contrat et les émetteurs **réellement écrits** dans le GDScript.

Il a trouvé **23 événements déclarés que rien n'appelle**. Ils sont visibles et
éditables dans le gestionnaire, avec des clips assignables — et ils ne joueront
jamais. Ce n'est pas cassé : ce n'est pas branché. Dette antérieure à ce
chantier, relevée ici parce qu'on y passait.

**Précision, ajoutée après coup.** `tools/audio_wiring_audit.py` existait déjà et
rapporte **0 muet** : il lit le *maître*, donc il demande « un son que l'auteur a
réellement configuré est-il émis ? ». Les deux outils ne posent pas la même
question et ne se contredisent pas :

| Outil | Question | Réponse |
| --- | --- | --- |
| `audio_wiring_audit.py` | un son **configuré** est-il émis ? | 0 muet — rien n'est cassé aujourd'hui |
| `audio_emitter_coverage_test.js` | un son **proposé** serait-il émis si on le configurait ? | non, pour 23 d'entre eux |

Aucun des 23 ne porte de clip aujourd'hui : rien ne se tait en jeu. Le risque est
le jour où l'auteur en remplira un. J'aurais dû chercher l'audit existant avant
d'écrire le mien ; les deux se justifient, mais le second aurait été plus court
s'il avait su que le premier existait.

| Groupe | Événements sans émetteur |
| --- | --- |
| Interface | `ui_hover` |
| Joueur | `player_hurt`, `player_heal`, `player_consume`, `player_equip`, `player_projectile_wall` |
| Monstres | `enemy_idle`, `enemy_projectile_fire`, `enemy_projectile_hit`, `enemy_projectile_wall` |
| Restaurant | `pickup`, `drop`, `clear_table`, `trash`, `customer_enter`, `customer_order`, `customer_leave_angry`, `takeaway`, `place_element`, `move_element`, `rotate_element`, `remove_element`, `door_open` |
| Donjon | `dungeon_dive_end` |

Le plus surprenant : **le héros tire avec un son, le monstre non.**
`player_projectile_fire` a son émetteur, `enemy_projectile_fire` n'en a pas.

Ils sont inscrits dans une liste d'exceptions **assumées et justifiées** plutôt
que le test désactivé : la dette reste visible, et aucune nouvelle ne peut
s'ajouter sans être écrite noir sur blanc. Le test refuse aussi une exception
devenue inutile, pour que la liste ne devienne pas un cimetière.

**Rien n'a été branché sans demande.** Écrire ces 23 émetteurs est un travail
séparé, à décider.

## Validation

| Contrôle | Résultat |
| --- | --- |
| `--area gameplay` | `encounter` vert, y compris les 30 assertions neuves |
| `--area audio` | `audio` et `audio_wiring` verts |
| `--area manager` | 120 contrôles, seuls les cinq échecs documentés |
| Smoke | 56 → 57 erreurs nommées, le seul ajout étant l'assertion instable de géométrie des gros monstres |

Rouge/vert fait une fois sur chaque garde neuve : l'entrée rejouée sur une
boucle, l'ordre des raisons d'attente, et l'émetteur du coup bloqué retiré.

## Reste du lot 6

- **6B — présentation** : silhouette ou effet de bouclier, signal de rupture,
  changement de posture ou de teinte, nom de phase sur la barre de boss. Ne pas
  compter uniquement sur une couleur.
- **6C — bac à sable** : phase, horloges et priorité en diagnostic dans la
  console, réglage de PV local, reset de rencontre, et la distinction entre
  « forcer la phase pour inspecter » et « jouer l'orchestration ».
- Les situations de référence (boss N1/N2, élite) dans une fixture explicite —
  jamais en remplaçant silencieusement des boss publiés.
