# 0005 — Ticks de plongée fixes et mises à jour typées - Statut : accepté - Date : 2026-08-10 - Remplace : aucun ## Contexte La plongée combine une physique Godot, des ressources continues, des dégâts ponctuels, des outils électriques, des alarmes et plusieurs présentations. Si chaque acteur ou interface utilise sa propre horloge ou recalcule une partie de ces règles, la pause, les variations de framerate et l'ordre des signaux peuvent produire des résultats différents. Les informations n'ont pas toutes la même nature. Une commande exprime une intention, la télémétrie décrit une mesure du monde, un snapshot décrit l'état autoritaire à un instant et un événement rapporte un fait survenu pendant une transition. Les regrouper dans un dictionnaire ou un signal générique rendrait leur propriétaire et leur temporalité implicites. ## Décision ### Horloge et cadence La simulation de plongée utilise une cadence de physique configurée à 60 Hz. L'application fournit exactement un `DiveStep` au domaine pour chaque tick de physique accepté. Le domaine n'accède ni à l'horloge système, ni au framerate, ni à l'arbre de scènes. Un `DiveTick` contient au minimum l'identifiant de session, une séquence strictement croissante et la durée du pas en secondes. La durée normale vaut `1 / 60` seconde. Un pas nul, négatif, non fini ou désordonné est refusé. Un pas supérieur à 0,1 seconde est subdivisé de manière déterministe par l'application ou refusé ; il n'est jamais appliqué comme un grand saut unique. La pause suspend la production de `DiveStep`. À la reprise, le temps passé en pause n'est ni simulé, ni rattrapé. Les tests utilisent des ticks construits explicitement et n'attendent jamais le temps réel. ### Contrats d'entrée séparés - `VehicleCommand` contient l'intention de propulsion issue du clavier ou d'un autre périphérique remappé. La souris n'oriente jamais le sous-marin. - `CameraLookCommand` contient uniquement le mouvement de regard dans la cabine. Il ne traverse pas le contrat de pilotage du véhicule. - `VehicleTelemetry` est un snapshot immuable produit par l'acteur physique : transform, vitesses, profondeur et efforts réellement appliqués. - `DiveTelemetrySample` réduit cette télémétrie aux mesures nécessaires aux règles de plongée. Le domaine ne reçoit aucun `Node` ou `Transform3D` Godot. - `CollisionObservation` décrit un impact ponctuel avec une séquence stable et sa vitesse normale en mètres par seconde. Les observations sont traitées de manière idempotente. - `ToolPowerDemand`, `EjectionIntent` et `ReturnRouteSample` décrivent séparément la demande électrique des outils, l'intention d'éjection et les données connues pour estimer le retour. - `DiveStep` agrège ces entrées pour un tick sans changer leur responsabilité. Ces frontières utilisent des objets typés. Les dictionnaires anonymes restent confinés aux codecs ou aux prototypes et ne traversent pas la production. ### Règles résolues et session Au début de chaque plongée, `DiveRulesResolver` construit un `ResolvedDiveRules` immuable en appliquant, dans cet ordre : 1. les règles de base ; 2. les capacités et améliorations du sous-marin ; 3. le profil de difficulté ; 4. les aides d'accessibilité qui modifient le gameplay ; 5. la validation et le bornage des valeurs. `DiveSession` est l'unique propriétaire de l'oxygène, de la batterie, de la coque, des seuils de pression, des alertes, de l'estimation de retour et de l'état terminal de la plongée. C'est un objet de domaine pur : il reçoit un `DiveStep`, applique les règles résolues dans un ordre fixe et retourne un `DiveUpdate`. L'ordre d'un pas est : validation, consommation, pression, collisions ordonnées, éjection, estimation du retour, alertes, état terminal, puis publication. Une session terminale ne rejoue pas sa conséquence et refuse toute mutation de gameplay ultérieure. ### Snapshot et événements Chaque pas accepté produit un seul `DiveUpdate` cohérent, associé à la session et à la séquence du tick. Il contient : - un `DiveSnapshot` immuable représentant l'état autoritaire après le pas ; - zéro ou plusieurs événements typés décrivant uniquement les faits survenus pendant ce pas. Le snapshot contient les valeurs actuelles et maximales, leurs unités et leurs ratios pour batterie, oxygène, coque, pression et profondeur, ainsi que l'estimation du retour, les alertes, l'éjection et l'éventuel motif terminal. Il permet à un nouvel écran de se synchroniser sans rejouer l'historique. Les événements, tels que `AlertLevelChanged`, `HullDamaged`, `ResourceDepleted` et `DiveTerminated`, déclenchent les réactions ponctuelles : audio, animation, journal ou transition applicative. Ils portent une identité ou une séquence permettant aux adaptateurs d'éviter un double traitement. Un événement ne remplace jamais une valeur persistante du snapshot. La présentation et l'audio consomment `DiveUpdate`. Ils ne recalculent ni seuils, ni dégâts, ni consommation, ni fin de plongée. Ils ne mutent jamais directement `DiveSession` ou l'acteur physique ; toute action repart sous forme de commande applicative. ## Conséquences - Une même suite de règles, commandes, télémétries et observations produit le même résultat dans les tests et dans le jeu. - La pause est une absence de ticks, pas un multiplicateur de temps dispersé entre plusieurs systèmes. - Le HUD peut afficher des valeurs exactes et des pourcentages sans posséder une copie concurrente des ressources. - L'audio peut réagir une seule fois aux transitions tout en consultant l'état courant à tout moment. - La conversion entre les types Godot et les données pures nécessite des adaptateurs explicites. - Ajouter une nouvelle entrée ou sortie de simulation demande un contrat nommé et des tests, mais évite les clés implicites et les dépendances de scène. - Les consommations indépendantes du tick de plongée, notamment les actions de port, restent des cas d'usage de campagne distincts. ## Alternatives considérées - **Chaque système utilise `_process(delta)`** : rejeté, car les règles dépendraient du framerate et leur ordre serait difficile à garantir. - **`DiveSession` lit directement la physique et l'horloge Godot** : rejeté, car le domaine ne serait plus pur ni testable sans scène. - **Un dictionnaire unique de données par frame** : rejeté, car commandes, mesures, états et événements auraient une temporalité implicite. - **Publier uniquement des événements** : rejeté, car un écran ouvert plus tard devrait reconstruire l'état en rejouant l'historique. - **Publier uniquement un snapshot** : rejeté, car les consommateurs devraient comparer deux états et réinventer les transitions ponctuelles. - **Rattraper les ticks après une pause ou un ralentissement important** : rejeté, car cela provoquerait des consommations et dégâts invisibles ou des pics de calcul au retour.