Applique .gitattributes sur tous les fichiers existants. Élimine les différences fantômes entre WSL et Windows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
22 KiB
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 :
- On part du haut (tier 4) et on descend
- Pour chaque palier, on calcule combien de ticks on peut consommer avant de passer au palier en-dessous
- 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 :
- Pour chaque tier, combien de points peut-on donner avant de descendre au palier suivant ?
- On prend le minimum entre les points disponibles et les points restants à donner
- 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 :
- Calculer la durée du segment
- Calculer les points gagnés dans ce segment avec
gainedIn - Vérifier si le cap (
ptsAllowed) est atteint → si oui, calculer le moment exact du gel et stopper - 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 segmentscurGl: niveau de jauge au moment du gel (ou àelsi 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ètreel(elapsed), il est ignoré en interne. La fonction appelle systématiquementelapsedLive(enc)pour ses calculs, ce qui garantit que les jauges continuent de progresser après la fin de session. Le paramètreelest conservé dans la signature pour la compatibilité des appels depuisenclosGlobalState.
Pour les jauges normales (sérénité, endurance, maturité, amour) :
- On récupère le snapshot de la jauge et de la stat au moment du démarrage du timer
- On appelle
computeGaugeState(startGl, recharges, ptsToAbsCap, elLive)pour obtenir les points gagnés en tenant compte des recharges et du gel - On applique la direction (
dir) :dir = +1(caresseur, foudroyeur, abreuvoir, dragofesse) : stat montedir = -1(baffeur) : stat descend
- On clamp la stat dans ses bornes (ex: sérénité entre -5000 et +5000)
- 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 :
- XP gagnée = via
computeGaugeState(tient compte des recharges et du gel à niveau 200) - XP nécessaire =
xpForLevel(cible) - xpForLevel(départ) - Niveau estimé =
levelFromXp(xpDépart + xpGagnée) - 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)
- Calcule
diff = cible - sérénité actuelle - Si
diff > 0→ besoin du caresseur (monte la sérénité) - Si
diff < 0→ besoin du baffeur (baisse la sérénité) - Temps =
timeToGain(niveauJauge, |diff|)
Version live (calcSerenEtaLive)
- Utilise
computeGaugeStatepour obtenir la sérénité estimée en temps réel (avec recharges et gel) - Recalcule le diff depuis cette estimation
- Calcule le temps restant avec les points encore à parcourir
10. ETA Niveau — calcLevelEta / calcLevelEtaLive
Version statique (calcLevelEta)
- XP nécessaire =
xpForLevel(cible) - xpForLevel(niveauActuel) - Cible =
dd.levelTarget ?? 200 - Temps =
timeToGain(niveauMangeoire, Math.min(xpNécessaire, niveauMangeoire))
Version live (calcLevelEtaLive)
- Cible =
dd.levelTarget ?? 200 - XP gagnée via
computeGaugeState(avec recharges et gel) - Niveau estimé =
levelFromXp(xpDépart + xpGagnée) - XP restante =
xpForLevel(cible) - xpForLevel(départ) - xpGagnée - 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 ?"
- Pour chaque DD × chaque jauge active → appeler
computeGaugeLive - Prendre le maximum de tous les countdowns = le plus long à terminer
- Compter combien de DD ont TOUTES leurs cibles atteintes (
ddDone) allDone = truesi 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 :
- Parcourt tous les enclos en cours (
summary.running = true) - Pour chacun, récupère l'état complet et appelle
enclosGlobalState - Si
allDone = true, exécutecomplete-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 }danstimer.gaugeRecharges[gid] - Plusieurs recharges s'accumulent dans un tableau
- Le
reset-timervide tous les tableaux de recharge
Impact sur le calcul
computeGaugeState segmente automatiquement le calcul entre chaque recharge :
- Segment 1 depuis
startGljusqu'à 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 :
- On part de la race cible et de la quantité voulue
- 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
- Pour chaque race nécessaire à cette génération :
- 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) :
- Lister tous les croisements possibles à cette génération
- 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émarragesnapStats[dd.id]: stats de chaque DD au moment du démarragegaugeRecharges: 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)
- Timer démarré →
start-timerprend les snapshots - Toutes les cibles atteintes →
complete-timerdéclenché - Session terminée : timer figé, bannière "Session terminée" visible, jauges continuent en fond
- Clic "🔄 Nouvelle fournée" →
nouvelle-fournee: reset complet + 1 DD neuve
Session enchaînée (nouvelles stats à monter)
- Session terminée (
alerted['__done__'] = true) - Les boutons de jauges sont déverrouillés → l'utilisateur sélectionne de nouvelles jauges
- L'utilisateur configure les niveaux de jauges pour la nouvelle session
- Clic "▶ Démarrer" →
start-timerdétectealerted['__done__']→ démarrage initial (pas reprise de pause) → nouveaux snapshots pris avec les stats actuelles des DD - 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
- Guard : si
enc.timer.running = falseou sienc.alerted['__done__']est déjà posé → retourne immédiatement (idempotent, pas de double alarme) - Pose
enc.timer.running = false - Pose
enc.timer.pausedAt = Date.now()— gèleelapsed()à l'instant de complétion - Pose
enc.alerted['__done__'] = true— active le mode "continuation en fond" danselapsedLive - Persiste l'état via
repo.save() - Émet l'événement
timer-completed→ déclenche alarme audio + notification Windows/mobile
Différence avec stop-timer (pause manuelle)
stop-timer: poserunning = falseetpausedAtmais ne pose pasalerted['__done__']- Conséquence :
elapsedLiveretourneelapsed()figé → toutes les jauges se figent sur la pause complete-timer:alerted['__done__'] = true→elapsedLivecontinue 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 autresxp-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.