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>
225 lines
8.9 KiB
TypeScript
Executable File
225 lines
8.9 KiB
TypeScript
Executable File
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);
|
||
});
|
||
});
|
||
});
|