- Suppression fichiers orphelins (task_plan, findings, progress, CHANGELOG) - Déplacement algorithmes.md → docs/algorithmes.md - CHANGELOG supprimé (doublon du README) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
470 lines
22 KiB
Markdown
Executable File
470 lines
22 KiB
Markdown
Executable File
# Algorithmes de calcul — Obsidienne
|
||
|
||
Ce document décrit tous les algorithmes utilisés dans l'application, expliqués simplement.
|
||
|
||
---
|
||
|
||
## 1. Système de tiers des jauges
|
||
|
||
Chaque jauge (baffeur, caresseur, foudroyeur, abreuvoir, dragofesse, mangeoire) a un **niveau** entre 0 et 100 000.
|
||
Ce niveau détermine un **tier** qui fixe la vitesse de conversion :
|
||
|
||
| Niveau de jauge | Tier | Taux (pts / 10 sec) |
|
||
|-----------------|------|---------------------|
|
||
| 0 – 40 000 | 1 | 10 |
|
||
| 40 001 – 70 000 | 2 | 20 |
|
||
| 70 001 – 90 000 | 3 | 30 |
|
||
| 90 001 – 100 000 | 4 | 40 |
|
||
|
||
**Principe** : La jauge se **vide** en donnant des points à la DD. Plus la jauge est haute, plus elle se vide vite (tier élevé = plus de points par tick).
|
||
|
||
Un "tick" = 10 secondes de timer.
|
||
|
||
---
|
||
|
||
## 2. Calcul des points gagnés dans le temps — `gainedIn(lvl, sec)`
|
||
|
||
**Question** : "Si ma jauge démarre au niveau `lvl` et tourne pendant `sec` secondes, combien de points ma DD gagne-t-elle ?"
|
||
|
||
**Algorithme** :
|
||
1. On part du haut (tier 4) et on descend
|
||
2. Pour chaque palier, on calcule combien de ticks on peut consommer avant de passer au palier en-dessous
|
||
3. On additionne les points de chaque palier traversé
|
||
|
||
**Exemple** : Jauge à 95 000, timer 60 sec (= 6 ticks)
|
||
- Tier 4 (90k–100k) : 5 000 pts dispo ÷ 40/tick = 125 ticks possibles → on en utilise 6 → gain = 6 × 40 = **240 pts**
|
||
- Jauge restante : 95 000 - 240 = 94 760
|
||
|
||
La jauge descend au fur et à mesure qu'elle donne des points. Quand elle passe sous un seuil de tier, le taux ralentit.
|
||
|
||
---
|
||
|
||
## 3. Temps pour gagner X points — `timeToGain(lvl, pts)`
|
||
|
||
**Question** : "Combien de temps faut-il pour gagner `pts` points depuis une jauge au niveau `lvl` ?"
|
||
|
||
C'est l'inverse de `gainedIn`. On parcourt les paliers du haut vers le bas :
|
||
1. Pour chaque tier, combien de points peut-on donner avant de descendre au palier suivant ?
|
||
2. On prend le minimum entre les points disponibles et les points restants à donner
|
||
3. On calcule le temps correspondant : `ceil(points / taux) × 10 sec`
|
||
|
||
Si la jauge se vide complètement avant d'atteindre l'objectif → retourne `Infinity` (impossible).
|
||
|
||
**Cas "vidange complète"** : `timeToGain(lvl, lvl)` donne le temps pour vider entièrement la jauge. C'est cette formule qu'utilisent à la fois l'affichage "Vide en" et le timer XP quand la cible est hors de portée d'une seule charge.
|
||
|
||
---
|
||
|
||
## 4. Niveau de jauge après X secondes — `gaugeAfter(lvl, sec)`
|
||
|
||
**Question** : "Si ma jauge démarre à `lvl` et tourne `sec` secondes, à quel niveau sera-t-elle ?"
|
||
|
||
Même logique que `gainedIn`, mais au lieu de compter les points donnés, on soustrait directement du niveau de la jauge.
|
||
|
||
---
|
||
|
||
## 5. Temps écoulé — `elapsed` et `elapsedLive`
|
||
|
||
### `elapsed(timer)` — temps figé pour l'affichage
|
||
|
||
Calcule les secondes écoulées depuis le démarrage du timer d'un enclos, en excluant les pauses. **Se fige** quand le timer est en pause ou terminé :
|
||
|
||
```
|
||
Si en cours : (maintenant - démarrage - temps_en_pause) / 1000
|
||
Si en pause : (moment_pause - démarrage - temps_en_pause) / 1000
|
||
Si non démarré : 0
|
||
```
|
||
|
||
Utilisé pour : l'affichage "Temps écoulé", le dashboard.
|
||
|
||
### `elapsedLive(enc)` — temps réel après complétion automatique
|
||
|
||
Retourne un temps qui **continue de progresser en temps réel** même après que la session s'est terminée automatiquement. Cela permet aux jauges de continuer à se vider en arrière-plan après l'alarme.
|
||
|
||
```
|
||
Si enc.alerted['__done__'] est positionné :
|
||
→ (Date.now() - startTime - pausedMs) / 1000 (jamais figé)
|
||
Sinon :
|
||
→ elapsed(enc.timer) (comportement normal, fige sur pause)
|
||
```
|
||
|
||
**`enc.alerted['__done__']`** est posé par la commande `complete-timer` quand toutes les cibles sont atteintes. Ce flag distingue une "fin naturelle de session" (jauges continuent) d'une "pause manuelle" (jauges figées).
|
||
|
||
Utilisé pour : tous les calculs de jauges dans `computeGaugeLive`, `enclosGaugeCurGl`, `calcSerenEtaLive`, `calcLevelEtaLive`.
|
||
|
||
---
|
||
|
||
## 6. Gel de jauge au cap absolu des stats et calcul par segments — `computeGaugeState`
|
||
|
||
### 6.1 Gel au cap absolu
|
||
|
||
Quand une **stat** atteint sa **limite absolue** (sérénité ±5000, endurance/maturité/amour 20 000), la jauge correspondante **s'arrête de se vider** — il n'y a plus rien à donner à la DD.
|
||
|
||
**Points jusqu'au cap** (`ptsToAbsCap`) :
|
||
- Stat normale : `dir>0 ? (statMax - startSt) : (startSt - statMin)` — la jauge continue **même après la cible**, jusqu'au cap absolu de la stat
|
||
- XP / mangeoire : `xpForLevel(200) - xpForLevel(niveauDépart)` — la jauge gèle uniquement au niveau 200, **jamais à la cible XP**
|
||
|
||
**Comportement après la cible** : les jauges ne s'arrêtent PAS quand la cible d'une DD est atteinte. Elles continuent de se vider jusqu'à ce que la stat atteigne son cap absolu (ex: sérénité à -5000, même si la cible était -60). Cela garantit que l'affichage reste cohérent et que les DDs continuent de progresser en fond.
|
||
|
||
**Affichage global de la jauge de stat (enclos)** : la jauge de stat se fige quand la **dernière** DD atteint son cap absolu. La mangeoire continue de se vider jusqu'au niveau 200.
|
||
|
||
### 6.2 Calcul par segments avec recharges — `computeGaugeState(startGl, recharges, ptsAllowed, el)`
|
||
|
||
Quand le joueur recharge une jauge en cours de session, le calcul se découpe en **segments** :
|
||
|
||
```
|
||
Segment 1 : startGl → recharge[0].atSec (ou fin du timer si pas de recharge)
|
||
Segment 2 : recharge[0].level → recharge[1].atSec
|
||
...
|
||
Segment N : recharge[N-1].level → el
|
||
```
|
||
|
||
Pour chaque segment :
|
||
1. Calculer la durée du segment
|
||
2. Calculer les points gagnés dans ce segment avec `gainedIn`
|
||
3. Vérifier si le cap (`ptsAllowed`) est atteint → si oui, calculer le moment exact du gel et stopper
|
||
4. Si non, passer au segment suivant avec le nouveau niveau de jauge après recharge
|
||
|
||
**Retourne** : `{ gained, curGl, effectiveEl }`
|
||
- `gained` : points totaux accumulés sur tous les segments
|
||
- `curGl` : niveau de jauge au moment du gel (ou à `el` si pas de gel)
|
||
- `effectiveEl` : temps réel de fonctionnement (≤ el, limité par le gel)
|
||
|
||
---
|
||
|
||
## 7. Calcul unifié d'une jauge — `computeGaugeLive(enc, dd, gid, el, started)`
|
||
|
||
C'est le **cœur du système**. Pour chaque DD et chaque jauge active, il calcule en temps réel :
|
||
- La stat estimée actuelle
|
||
- Si la cible est atteinte
|
||
- Le % de progression
|
||
- Le countdown restant
|
||
|
||
> **Note sur le paramètre `el`** : bien que la signature accepte un paramètre `el` (elapsed), il est **ignoré en interne**. La fonction appelle systématiquement `elapsedLive(enc)` pour ses calculs, ce qui garantit que les jauges continuent de progresser après la fin de session. Le paramètre `el` est conservé dans la signature pour la compatibilité des appels depuis `enclosGlobalState`.
|
||
|
||
### Pour les jauges normales (sérénité, endurance, maturité, amour) :
|
||
|
||
1. On récupère le **snapshot** de la jauge et de la stat au moment du démarrage du timer
|
||
2. On appelle `computeGaugeState(startGl, recharges, ptsToAbsCap, elLive)` pour obtenir les points gagnés en tenant compte des recharges et du gel
|
||
3. On applique la direction (`dir`) :
|
||
- `dir = +1` (caresseur, foudroyeur, abreuvoir, dragofesse) : stat monte
|
||
- `dir = -1` (baffeur) : stat descend
|
||
4. On clamp la stat dans ses bornes (ex: sérénité entre -5000 et +5000)
|
||
5. On calcule le temps total et le countdown via `timeToGain`
|
||
|
||
### Pour la mangeoire (XP) — même modèle que les autres jauges :
|
||
|
||
La mangeoire se vide exactement comme les autres jauges (même tiers, même dégression). L'XP suit le même algorithme :
|
||
|
||
1. XP gagnée = via `computeGaugeState` (tient compte des recharges et du gel à niveau 200)
|
||
2. XP nécessaire = `xpForLevel(cible) - xpForLevel(départ)`
|
||
3. Niveau estimé = `levelFromXp(xpDépart + xpGagnée)`
|
||
4. Countdown = `timeToGain(curGl, Math.min(xpRestante, curGl))` — si la cible dépasse la capacité de la jauge, on affiche le temps de vidange complète
|
||
|
||
**Cible XP par défaut** : si `dd.levelTarget === null`, on cible le niveau 200.
|
||
|
||
**Exemple** : mangeoire à 85 000 (tier 3) pendant 7 000 sec (700 ticks) :
|
||
- 500 ticks à 30 XP/tick (tier 3, 70k→85k) = 15 000 XP, jauge → 70 000
|
||
- 200 ticks à 20 XP/tick (tier 2) = 4 000 XP, jauge → 66 000
|
||
- Total = **19 000 XP** (contre 21 000 XP avec un taux fixe erroné)
|
||
|
||
---
|
||
|
||
## 8. Table d'XP et niveaux
|
||
|
||
### Table XP_RAW
|
||
|
||
Dictionnaire de 200 entrées : `niveau → XP cumulatif total`.
|
||
|
||
Exemples :
|
||
- Niveau 1 → 0 XP
|
||
- Niveau 10 → 809 XP
|
||
- Niveau 50 → 34 365 XP
|
||
- Niveau 100 → 172 668 XP
|
||
- Niveau 200 → 867 582 XP
|
||
|
||
Les valeurs sont **cumulatives** (pas incrémentales).
|
||
|
||
### `xpForLevel(lvl)` : Retourne l'XP cumulatif pour atteindre le niveau `lvl`.
|
||
|
||
### `levelFromXp(xp)` : Retourne le plus haut niveau atteint avec `xp` points d'XP cumulatifs. Parcourt la table de 200 à 1 pour trouver le seuil.
|
||
|
||
---
|
||
|
||
## 9. ETA Sérénité — `calcSerenEta` / `calcSerenEtaLive`
|
||
|
||
### Version statique (`calcSerenEta`)
|
||
1. Calcule `diff = cible - sérénité actuelle`
|
||
2. Si `diff > 0` → besoin du caresseur (monte la sérénité)
|
||
3. Si `diff < 0` → besoin du baffeur (baisse la sérénité)
|
||
4. Temps = `timeToGain(niveauJauge, |diff|)`
|
||
|
||
### Version live (`calcSerenEtaLive`)
|
||
1. Utilise `computeGaugeState` pour obtenir la sérénité estimée en temps réel (avec recharges et gel)
|
||
2. Recalcule le diff depuis cette estimation
|
||
3. Calcule le temps restant avec les points encore à parcourir
|
||
|
||
---
|
||
|
||
## 10. ETA Niveau — `calcLevelEta` / `calcLevelEtaLive`
|
||
|
||
### Version statique (`calcLevelEta`)
|
||
1. XP nécessaire = `xpForLevel(cible) - xpForLevel(niveauActuel)`
|
||
2. Cible = `dd.levelTarget ?? 200`
|
||
3. Temps = `timeToGain(niveauMangeoire, Math.min(xpNécessaire, niveauMangeoire))`
|
||
|
||
### Version live (`calcLevelEtaLive`)
|
||
1. Cible = `dd.levelTarget ?? 200`
|
||
2. XP gagnée via `computeGaugeState` (avec recharges et gel)
|
||
3. Niveau estimé = `levelFromXp(xpDépart + xpGagnée)`
|
||
4. XP restante = `xpForLevel(cible) - xpForLevel(départ) - xpGagnée`
|
||
5. Countdown = `timeToGain(curGl, Math.min(xpRestante, curGl))`
|
||
- Si XP restante ≤ capacité de la jauge : temps pour atteindre la cible
|
||
- Sinon : temps de vidange complète (cohérent avec "Vide en")
|
||
|
||
---
|
||
|
||
## 11. Countdown global d'un enclos — `enclosGlobalState(enc)`
|
||
|
||
**Question** : "Dans combien de temps TOUTES les DD de cet enclos auront atteint TOUTES leurs cibles ?"
|
||
|
||
1. Pour chaque DD × chaque jauge active → appeler `computeGaugeLive`
|
||
2. Prendre le **maximum** de tous les countdowns = le plus long à terminer
|
||
3. Compter combien de DD ont TOUTES leurs cibles atteintes (`ddDone`)
|
||
4. `allDone = true` si **toutes les jauges actives** de **toutes les DDs** ont atteint leur cible
|
||
|
||
**Règle d'alarme unique** : la session se termine (`complete-timer`) une seule fois, au timer le plus long. Si un enclos a baffeur + mangeoire, l'alarme ne sonne que quand les DEUX cibles (sérenité ET niveau XP) sont atteintes pour toutes les DDs. Il n'y a pas d'alarme intermédiaire quand une seule cible est atteinte.
|
||
|
||
**Prévisualisation avant démarrage** : la fonction calcule même quand le timer n'est pas démarré (`started = false`, `el = 0`). L'"Alarme dans" et les timers DD se mettent à jour en temps réel dès que le joueur saisit une valeur de jauge ou une cible — sans avoir à démarrer le timer.
|
||
|
||
## 11 bis. Vérification globale de complétion — `checkAllEnclosCompletion()` dans `App`
|
||
|
||
La vérification de complétion ne dépend pas de la vue active ni du focus de la fenêtre.
|
||
|
||
**Pourquoi pas dans la boucle rAF ?** `requestAnimationFrame` se **suspend** quand l'application Electron perd le focus OS (ex : l'utilisateur alt-tab vers une autre application). Si la détection était dans la boucle rAF, la notification ne sonnerait jamais quand l'utilisateur est hors de l'app.
|
||
|
||
**Solution** : `checkAllEnclosCompletion()` tourne dans un `window.setInterval(..., 1000)` indépendant, démarré dans `App.render()` et nettoyé dans `App.destroy()`. `setInterval` continue de s'exécuter même quand la fenêtre est en arrière-plan.
|
||
|
||
**Algorithme** :
|
||
1. Parcourt **tous** les enclos en cours (`summary.running = true`)
|
||
2. Pour chacun, récupère l'état complet et appelle `enclosGlobalState`
|
||
3. Si `allDone = true`, exécute `complete-timer` → alarme + notification immédiate
|
||
|
||
**Double protection** : `EnclosView.update()` appelle aussi `complete-timer` quand `allDone && running` (quand l'utilisateur est sur l'enclos). Le handler `complete-timer` possède un guard `if (!enc.timer.running || enc.alerted['__done__']) return` pour éviter un double déclenchement.
|
||
|
||
---
|
||
|
||
## 12. Recharge de jauge en cours de session
|
||
|
||
Le joueur peut **recharger une jauge pendant que le timer tourne** (ex : remplir la mangeoire à mi-session).
|
||
|
||
### Enregistrement (`recharge-gauge`)
|
||
|
||
- Stocke `{ atSec: elapsed(), level: newLevel }` dans `timer.gaugeRecharges[gid]`
|
||
- Plusieurs recharges s'accumulent dans un tableau
|
||
- Le `reset-timer` vide tous les tableaux de recharge
|
||
|
||
### Impact sur le calcul
|
||
|
||
`computeGaugeState` segmente automatiquement le calcul entre chaque recharge :
|
||
- Segment 1 depuis `startGl` jusqu'à la première recharge
|
||
- Segment 2 depuis le nouveau niveau jusqu'à la recharge suivante (ou la fin)
|
||
- etc.
|
||
|
||
Le gel au cap absolu est vérifié dans chaque segment : si la stat atteint son cap avant la prochaine recharge, le gel est précis à la seconde près.
|
||
|
||
### Affichage
|
||
|
||
Les inputs de jauge affichent une bordure verte (classe `.gauge-inp-recharge`) pendant que le timer tourne, indiquant que toute nouvelle saisie sera interprétée comme une recharge.
|
||
|
||
---
|
||
|
||
## 13. Mises à jour en temps réel des inputs
|
||
|
||
Tous les champs de saisie (niveaux de jauges, stats des DD, cibles) déclenchent une mise à jour de l'état à **chaque frappe** via un listener `input`, en plus du `blur` final. Cela permet :
|
||
|
||
- L'"Alarme dans" de se recalculer instantanément pendant la saisie
|
||
- Les timers DD (XP, sérénité, etc.) de se mettre à jour à la volée
|
||
- Une expérience cohérente avant ET pendant le timer
|
||
|
||
Exception : les recharges de jauge ne sont déclenchées que sur `blur`/`Enter` (pas sur chaque frappe) pour éviter d'enregistrer des recharges partielles.
|
||
|
||
---
|
||
|
||
## 14. Arbre de réapprovisionnement — `calcAppro()`
|
||
|
||
**Question** : "Pour produire Q exemplaires de la race X, de quelles races et en quelles quantités ai-je besoin ?"
|
||
|
||
### Principe : décomposition récursive par génération
|
||
|
||
Chaque race de génération ≥ 2 est produite par le croisement de 2 races parentes (table `BREEDING_RECIPES`).
|
||
|
||
**Algorithme** :
|
||
1. On part de la race cible et de la quantité voulue
|
||
2. Pour chaque génération (de la plus haute à gen 2) :
|
||
- Pour chaque race nécessaire à cette génération :
|
||
- Calculer le nombre de couples nécessaires (voir §14.1)
|
||
- Ajouter les parents nécessaires (chacun × nombre de couples) dans le pool
|
||
3. Les races gen 1 restantes = matières premières
|
||
|
||
### 14.1 Mécanisme du reproducteur
|
||
|
||
Un reproducteur est une DD réutilisable : elle peut faire plusieurs bébés.
|
||
|
||
```
|
||
Si 2×R ≥ Q → couples = ceil(Q / 2)
|
||
Sinon → couples = Q - R
|
||
```
|
||
|
||
Où `R` = nombre de reproducteurs, `Q` = quantité nécessaire.
|
||
|
||
---
|
||
|
||
## 15. Calcul d'inventaire avec contraintes ♂/♀ — `calcInventaire()`
|
||
|
||
**Question** : "Avec mon stock actuel de DD (mâles et femelles), quels croisements puis-je réaliser ?"
|
||
|
||
### Modèle de données
|
||
Chaque race dans l'inventaire : `{ m: mâles, f: femelles, n: neutres }`
|
||
|
||
### Algorithme : round-robin par génération
|
||
|
||
Pour chaque génération (2 → 10) :
|
||
1. Lister tous les croisements possibles à cette génération
|
||
2. **Boucle round-robin** : tant qu'au moins un croisement est possible :
|
||
- Vérifier qu'on a 1 mâle-capable chez A ET 1 femelle-capable chez B (ou l'inverse)
|
||
- Consommer les parents (priorité au stock réel m/f, puis les neutres n)
|
||
- Ajouter 1 neutre (`n++`) à la race du bébé produit
|
||
|
||
### Priorité de consommation
|
||
```
|
||
takeMale : si m > 0 → m--, sinon n--
|
||
takeFemale : si f > 0 → f--, sinon n--
|
||
```
|
||
|
||
---
|
||
|
||
## 16. Constantes des stats
|
||
|
||
| Stat | Min | Max | Jauge(s) associée(s) |
|
||
|-----------|--------|--------|----------------------|
|
||
| Sérénité | -5 000 | 5 000 | Baffeur (↓), Caresseur (↑) |
|
||
| Endurance | 0 | 20 000 | Foudroyeur (↑) |
|
||
| Maturité | 0 | 20 000 | Abreuvoir (↑) |
|
||
| Amour | 0 | 20 000 | Dragofesse (↑) |
|
||
| Niveau/XP | 1 | 200 | Mangeoire (↑) |
|
||
|
||
---
|
||
|
||
## 17. Système de snapshots
|
||
|
||
Quand le timer démarre (démarrage initial uniquement, pas lors d'une reprise de pause) :
|
||
- `snapGauges` : niveaux de toutes les jauges actives au moment du démarrage
|
||
- `snapStats[dd.id]` : stats de chaque DD au moment du démarrage
|
||
- `gaugeRecharges` : réinitialisé à `{}`
|
||
|
||
Tous les calculs utilisent ces snapshots comme point de départ. Une **reprise de pause** accumule uniquement `pausedMs` sans toucher aux snapshots.
|
||
|
||
---
|
||
|
||
## 18. Flux de session — enchaînement des sessions
|
||
|
||
### Session unique (sans jauges supplémentaires)
|
||
1. Timer démarré → `start-timer` prend les snapshots
|
||
2. Toutes les cibles atteintes → `complete-timer` déclenché
|
||
3. Session terminée : timer figé, bannière "Session terminée" visible, jauges continuent en fond
|
||
4. Clic "🔄 Nouvelle fournée" → `nouvelle-fournee` : reset complet + 1 DD neuve
|
||
|
||
### Session enchaînée (nouvelles stats à monter)
|
||
1. Session terminée (`alerted['__done__'] = true`)
|
||
2. Les **boutons de jauges sont déverrouillés** → l'utilisateur sélectionne de nouvelles jauges
|
||
3. L'utilisateur configure les niveaux de jauges pour la nouvelle session
|
||
4. Clic "▶ Démarrer" → `start-timer` détecte `alerted['__done__']` → **démarrage initial** (pas reprise de pause) → nouveaux snapshots pris avec les stats actuelles des DD
|
||
5. Nouvelle session démarre : les stats des DD reflètent les gains de la session précédente
|
||
|
||
**Règle de déverrouillage des jauges** : `locked = started && !enc.alerted['__done__']`
|
||
Les jauges sont verrouillées uniquement pendant une session active (running ou en pause manuelle). Elles sont déverrouillées une fois la session terminée automatiquement.
|
||
|
||
**Bouton timer** : affiche "▶ Reprendre" uniquement en cas de pause manuelle (`pausedAt` et `!alerted['__done__']`). Après complétion automatique, affiche "▶ Démarrer" (nouvelle session).
|
||
|
||
---
|
||
|
||
## 19 bis. Commande `nouvelle-fournee`
|
||
|
||
Remet l'enclos dans un état "vierge" pour une nouvelle fournée complète :
|
||
- Reset timer (efface startTime, snapshots, alerted, recharges)
|
||
- Remet tous les niveaux de jauges à 0
|
||
- Supprime toutes les DDs
|
||
- Ajoute 1 nouvelle DD avec les stats de base (`serenite: 0, endurance: 0, maturite: 0, amour: 0, xp: 1`)
|
||
|
||
Distinct de `reset-timer` (qui remet seulement le timer à zéro, conserve les DDs et les jauges) et de `clear-enclos` (qui remet tout à zéro incluant les jauges actives et le nom).
|
||
|
||
---
|
||
|
||
## 18. Cycle de vie de `complete-timer`
|
||
|
||
La commande `complete-timer` représente la **fin naturelle d'une session** (toutes les cibles atteintes). Elle est distincte d'un simple `stop-timer` (pause manuelle).
|
||
|
||
### Ce que fait `complete-timer`
|
||
1. **Guard** : si `enc.timer.running = false` ou si `enc.alerted['__done__']` est déjà posé → retourne immédiatement (idempotent, pas de double alarme)
|
||
2. Pose `enc.timer.running = false`
|
||
3. Pose `enc.timer.pausedAt = Date.now()` — gèle `elapsed()` à l'instant de complétion
|
||
4. Pose `enc.alerted['__done__'] = true` — active le mode "continuation en fond" dans `elapsedLive`
|
||
5. Persiste l'état via `repo.save()`
|
||
6. Émet l'événement `timer-completed` → déclenche alarme audio + notification Windows/mobile
|
||
|
||
### Différence avec `stop-timer` (pause manuelle)
|
||
- `stop-timer` : pose `running = false` et `pausedAt` mais **ne pose pas** `alerted['__done__']`
|
||
- Conséquence : `elapsedLive` retourne `elapsed()` figé → toutes les jauges se figent sur la pause
|
||
- `complete-timer` : `alerted['__done__'] = true` → `elapsedLive` continue en temps réel → jauges continuent de se vider en fond
|
||
|
||
### Après `complete-timer`
|
||
| Élément | Comportement |
|
||
|---------|-------------|
|
||
| Affichage "Temps écoulé" (enclos + dashboard) | **Figé** — utilise `elapsed(timer)` = `(pausedAt - startTime - pausedMs) / 1000` |
|
||
| Jauges (niveau, barre) | **Continuent** — `enclosGaugeCurGl` utilise `elapsedLive` |
|
||
| Stats estimées des DD | **Continuent** — `computeGaugeLive` utilise `elapsedLive` |
|
||
| Countdown "Alarme dans" | Affiche `✅` |
|
||
| Bannière "Session terminée" | Visible dans la vue enclos |
|
||
|
||
---
|
||
|
||
## 19. Animations et détection de tick — `DragodindeCard`
|
||
|
||
### Détection de tick
|
||
|
||
Un "tick" survient toutes les 10 secondes. La carte DD détecte un nouveau tick en comparant `Math.floor(elapsedLive(enc) / 10)` avec la valeur précédente.
|
||
|
||
Utiliser `elapsedLive(enc)` (et non `elapsed`) garantit que les animations de tick **continuent** après la fin de session, le temps que toutes les jauges se vident en arrière-plan.
|
||
|
||
### `deltaActive` — quand afficher l'animation de delta
|
||
|
||
L'animation de delta ("+20 xp", "-10 sérenité"…) s'affiche à chaque tick **uniquement si la jauge est encore active**. Une jauge est considérée inactive quand la stat atteint son **cap absolu** (pas sa cible) :
|
||
|
||
```
|
||
Pour les jauges de stats : atAbsCap = (estStat >= statMax) ou (estStat <= statMin)
|
||
Pour la mangeoire (XP) : atAbsCap = (estLevel >= 200)
|
||
```
|
||
|
||
Cela signifie que l'animation continue après la cible de l'utilisateur (ex: sérénité cible -60 → animation continue jusqu'à -5000).
|
||
|
||
### Badge "✓ TERMINÉ"
|
||
|
||
Affiché sur la carte DD quand **toutes les jauges actives** ont `done = true` pour cette DD. Les jauges de stats et la mangeoire comptent toutes de la même façon — pas de distinction.
|
||
|
||
---
|
||
|
||
## 20. Événements domaine actifs
|
||
|
||
| Événement | Déclencheur | Effet |
|
||
|-----------|-------------|-------|
|
||
| `timer-completed` | `complete-timer` (toutes cibles atteintes) | Alarme audio + notification Windows + notification mobile ntfy |
|
||
| `gauge-threshold-reached` | Franchissement d'un palier de jauge | (usage interne) |
|
||
| `accouplement-registered` | Enregistrement d'un accouplement | Mise à jour des stats globales |
|
||
| `enclos-deleted` | Suppression d'un enclos | Nettoyage de l'état |
|
||
|
||
**Événements supprimés** (ancienne implémentation, ne plus utiliser) :
|
||
- `target-reached` — ancienne alarme intermédiaire quand une cible de stat était atteinte avant les autres
|
||
- `xp-target-reached` — ancienne alarme séparée pour la cible XP
|
||
|
||
Ces événements ont été remplacés par la règle d'alarme unique : **une seule alarme au timer le plus long**, déclenchée par `timer-completed` uniquement.
|