# Algorithmes de calcul — Minuteur Dragodinde 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.