import { describe, it, expect } from 'vitest'; import { gainedIn, timeToGain, gaugeAfter, elapsed, computeGaugeState, TimerState } from '@domain/services/GaugeCalculator'; import type { GaugeRecharge } from '@domain/services/GaugeCalculator'; describe('GaugeCalculator', () => { describe('gainedIn', () => { it('tier 1 only: 100 pts in 100 sec', () => { // Level 100, 100 sec = 10 ticks, rate 10 → gain 100 expect(gainedIn(100, 100)).toBe(100); }); it('zero seconds = zero gain', () => { expect(gainedIn(50000, 0)).toBe(0); }); it('level 0 = zero gain', () => { expect(gainedIn(0, 100)).toBe(0); }); it('crosses tier boundary 2→1', () => { // Level 40010, 10 sec = 1 tick. In tier 2 (40001-70000), rate 20. // But only 10 pts above 40000, so 10/20 = 0 full ticks at rate 20 → 0 gained from tier 2 // Actually: a = 40010-40000 = 10, m = floor(10/20) = 0, u = 0. Move to tier 1. // Tier 1: g=40010 > 0, a=40010-0=40010... wait g is now 40010 still since u=0 // Let me recalculate: g=40010, tier {lo:40000, r:20}: a=10, m=0, u=0. No drain. // Tier {lo:0, r:10}: g=40010 > 0, a=40010, m=4001, u=min(4001,1)=1, out=10 // Actually g should still be 40010 since we didn't drain. Hmm but the loop says if g<=lo continue // g=40010 > 40000 → process. a=10, m=floor(10/20)=0, u=0. // g=40010 > 0 → process. a=40010, m=floor(40010/10)=4001, u=min(4001,1)=1, out=10. expect(gainedIn(40010, 10)).toBe(10); }); it('full tier 4 drain', () => { // Level 100000, 250 ticks needed to drain tier 4 (10000/40=250) // 250 ticks = 2500 sec expect(gainedIn(100000, 2500)).toBe(10000); }); it('large gauge drains through multiple tiers', () => { // Level 100000, 5000 sec = 500 ticks // Tier 4: 100000→90000 = 10000/40 = 250 ticks → gain 10000, tl=250 // Tier 3: 90000→70000 = 20000/30 = 666 ticks, but only 250 left → 250*30 = 7500 // Total = 17500 expect(gainedIn(100000, 5000)).toBe(17500); }); it('negative level clamped to 0', () => { expect(gainedIn(-100, 100)).toBe(0); }); it('level above 100000 clamped', () => { expect(gainedIn(150000, 10)).toBe(40); // tier 4 rate }); }); describe('timeToGain', () => { it('zero points = zero time', () => { expect(timeToGain(50000, 0)).toBe(0); }); it('negative points = zero time', () => { expect(timeToGain(50000, -10)).toBe(0); }); it('tier 1: 100 pts needs 100 sec', () => { // Level 100, need 100 pts at rate 10 → 10 ticks → 100 sec expect(timeToGain(100, 100)).toBe(100); }); it('tier 2: 1000 pts from level 50000', () => { // Level 50000, tier 2 rate 20. 1000/20 = 50 ticks → 500 sec expect(timeToGain(50000, 1000)).toBe(500); }); it('returns Infinity if level is 0', () => { expect(timeToGain(0, 100)).toBe(Infinity); }); }); describe('gaugeAfter', () => { it('no time = same level', () => { expect(gaugeAfter(50000, 0)).toBe(50000); }); it('tier 1 full drain', () => { // Level 100, 100 sec = 10 ticks. 100/10=10 ticks needed. Exactly drains to 0. expect(gaugeAfter(100, 100)).toBe(0); }); it('does not go below 0', () => { expect(gaugeAfter(50, 1000)).toBe(0); }); it('partial drain in tier 2', () => { // Level 50000, 10 sec = 1 tick. Rate 20 at tier 2. 50000-20 = 49980 expect(gaugeAfter(50000, 10)).toBe(49980); }); }); describe('computeGaugeState', () => { const NO_RECHARGES: GaugeRecharge[] = []; it('sans recharge ni cap : identique à gainedIn', () => { const el = 1000; const startGl = 50000; const { gained, curGl, effectiveEl } = computeGaugeState(startGl, NO_RECHARGES, Infinity, el); expect(gained).toBe(gainedIn(startGl, el)); expect(curGl).toBe(gaugeAfter(startGl, el)); expect(effectiveEl).toBe(el); }); it('cap à 0 dès le départ (stat déjà au max) → gel immédiat', () => { const { gained, effectiveEl } = computeGaugeState(90000, NO_RECHARGES, 0, 5000); expect(gained).toBe(0); expect(effectiveEl).toBe(0); }); it('cap atteint en cours de segment → gel précis', () => { // Tier 2 (50000), rate 20/tick. On veut 200 pts → 10 ticks → 100 sec. const { gained, effectiveEl } = computeGaugeState(50000, NO_RECHARGES, 200, 5000); expect(gained).toBe(200); expect(effectiveEl).toBe(100); // timeToGain(50000, 200) = 100 sec }); it('une recharge avant la fin, pas de cap → somme des deux segments', () => { // Seg1 : 50000, 100 sec → gainedIn(50000, 100) = 10 ticks × 20 = 200 pts // Recharge à 80000 à t=100 // Seg2 : 80000, 100 sec → gainedIn(80000, 100) = 10 ticks × 30 = 300 pts // Total = 500 pts const recharges: GaugeRecharge[] = [{ atSec: 100, level: 80000 }]; const { gained, effectiveEl } = computeGaugeState(50000, recharges, Infinity, 200); expect(gained).toBe(500); expect(effectiveEl).toBe(200); }); it('cap atteint dans le premier segment (avant la recharge)', () => { // Seg1 : 50000, cap=100 pts → gel à timeToGain(50000,100)=50 sec // Recharge à t=100 ne doit pas être prise en compte const recharges: GaugeRecharge[] = [{ atSec: 100, level: 90000 }]; const { gained, effectiveEl } = computeGaugeState(50000, recharges, 100, 500); expect(gained).toBe(100); expect(effectiveEl).toBe(50); // gel avant la recharge }); it('cap atteint dans le deuxième segment (après une recharge)', () => { // Seg1 : 50000, 100 sec → 200 pts, cap=500 → pas encore atteint // Recharge à 80000 à t=100 // Seg2 : 80000, cap restant=300 pts → timeToGain(80000,300)=100 sec → effectiveEl=100+100=200 const recharges: GaugeRecharge[] = [{ atSec: 100, level: 80000 }]; const { gained, effectiveEl } = computeGaugeState(50000, recharges, 500, 9999); expect(gained).toBe(500); expect(effectiveEl).toBe(200); }); it('plusieurs recharges successives', () => { // Seg1 : 10000 (tier1), 50 sec → 5 ticks × 10 = 50 pts // Recharge à 50000 à t=50 // Seg2 : 50000 (tier2), 50 sec → 5 ticks × 20 = 100 pts // Recharge à 90000 à t=100 // Seg3 : 90000 (tier3), 50 sec → 5 ticks × 30 = 150 pts // Total = 300 pts const recharges: GaugeRecharge[] = [ { atSec: 50, level: 50000 }, { atSec: 100, level: 90000 }, ]; const { gained } = computeGaugeState(10000, recharges, Infinity, 150); expect(gained).toBe(300); }); it('recharge après le cap → le cap prime, recharge ignorée', () => { // Cap à 50 pts → gel à 50 sec (tier2, rate20, 50/20=2.5→3 ticks=30sec... wait) // timeToGain(50000, 50) = ceil(50/20)*10 = 3*10 = 30 sec // Recharge à t=100 → ignorée car gel à t=30 const recharges: GaugeRecharge[] = [{ atSec: 100, level: 90000 }]; const { gained, effectiveEl } = computeGaugeState(50000, recharges, 50, 9999); expect(gained).toBe(50); expect(effectiveEl).toBe(30); }); it('curGl reflète le niveau après le gel', () => { // Tier2 (50000), cap=200 pts → 10 ticks → 100 sec, gauge = 50000-10*20=49800 const { curGl } = computeGaugeState(50000, NO_RECHARGES, 200, 9999); expect(curGl).toBe(49800); }); it('curGl reflète la recharge dans le deuxième segment', () => { // Seg1: 50000, 100sec → curGl après seg1 = gaugeAfter(50000,100)=49800 mais recharge à 80000 // Seg2: 80000, no cap → curGl = gaugeAfter(80000, 100sec) = 80000-10*30=79700 const recharges: GaugeRecharge[] = [{ atSec: 100, level: 80000 }]; const { curGl } = computeGaugeState(50000, recharges, Infinity, 200); expect(curGl).toBe(gaugeAfter(80000, 100)); }); }); describe('elapsed', () => { it('no start time = 0', () => { const t: TimerState = { startTime: null, running: false, pausedAt: null, pausedMs: 0 }; expect(elapsed(t)).toBe(0); }); it('running timer', () => { const now = Date.now(); const t: TimerState = { startTime: now - 10000, running: true, pausedAt: null, pausedMs: 0 }; const el = elapsed(t); expect(el).toBeGreaterThanOrEqual(9.9); expect(el).toBeLessThanOrEqual(10.5); }); it('paused timer', () => { const now = Date.now(); const t: TimerState = { startTime: now - 10000, running: false, pausedAt: now - 5000, pausedMs: 0 }; const el = elapsed(t); expect(el).toBeCloseTo(5, 0); }); it('paused timer with accumulated pause', () => { const now = Date.now(); const t: TimerState = { startTime: now - 20000, running: false, pausedAt: now - 5000, pausedMs: 5000 }; // elapsed = (pausedAt - startTime - pausedMs) / 1000 = (15000 - 5000) / 1000 = 10 expect(elapsed(t)).toBeCloseTo(10, 0); }); }); });