import { Clock } from '../shared/clock'; import { CharacterVitalsService } from './character-vitals.service'; import { Character } from './entities/character.entity'; function fakeClock(initialIso: string): { clock: Clock; advanceSeconds: (seconds: number) => void; } { let current = Date.parse(initialIso); return { clock: { now: () => new Date(current) }, advanceSeconds: (seconds: number) => { current += seconds * 1000; }, }; } function character(overrides: Partial = {}): Character { return { id: 'character-1', currentHp: 50, hpRegenSince: null, ...overrides, } as Character; } describe('CharacterVitalsService', () => { describe('effectiveHp', () => { it('returns the raw current HP when regeneration is paused', () => { const { clock } = fakeClock('2026-08-21T12:00:00.000Z'); const service = new CharacterVitalsService(clock); const hp = service.effectiveHp(character({ currentHp: 37, hpRegenSince: null }), 100); expect(hp).toBe(37); }); it('adds one HP per elapsed second since the anchor', () => { const { clock, advanceSeconds } = fakeClock('2026-08-21T12:00:00.000Z'); const service = new CharacterVitalsService(clock); const anchor = new Date('2026-08-21T12:00:00.000Z'); const target = character({ currentHp: 40, hpRegenSince: anchor }); advanceSeconds(25); expect(service.effectiveHp(target, 100)).toBe(65); }); it('floors partial seconds instead of rounding up', () => { const { clock, advanceSeconds } = fakeClock('2026-08-21T12:00:00.000Z'); const service = new CharacterVitalsService(clock); const anchor = new Date('2026-08-21T12:00:00.000Z'); const target = character({ currentHp: 40, hpRegenSince: anchor }); advanceSeconds(1.9); expect(service.effectiveHp(target, 100)).toBe(41); }); it('clamps regeneration at maxHp', () => { const { clock, advanceSeconds } = fakeClock('2026-08-21T12:00:00.000Z'); const service = new CharacterVitalsService(clock); const anchor = new Date('2026-08-21T12:00:00.000Z'); const target = character({ currentHp: 90, hpRegenSince: anchor }); advanceSeconds(50); expect(service.effectiveHp(target, 100)).toBe(100); }); it('never lets HP fall if the clock moves backwards', () => { const clock: Clock = { now: () => new Date('2026-08-21T11:59:00.000Z') }; const service = new CharacterVitalsService(clock); const anchor = new Date('2026-08-21T12:00:00.000Z'); const target = character({ currentHp: 40, hpRegenSince: anchor }); expect(service.effectiveHp(target, 100)).toBe(40); }); }); describe('pause', () => { it('freezes current HP at the given value and clears the anchor', () => { const { clock } = fakeClock('2026-08-21T12:00:00.000Z'); const service = new CharacterVitalsService(clock); const target = character({ currentHp: 100, hpRegenSince: new Date('2026-08-21T11:00:00.000Z'), }); service.pause(target, 62); expect(target.currentHp).toBe(62); expect(target.hpRegenSince).toBeNull(); }); }); describe('resume', () => { it('sets current HP and anchors regeneration at now', () => { const { clock } = fakeClock('2026-08-21T12:00:00.000Z'); const service = new CharacterVitalsService(clock); const target = character({ currentHp: 0, hpRegenSince: null }); service.resume(target, 15); expect(target.currentHp).toBe(15); expect(target.hpRegenSince).toEqual(new Date('2026-08-21T12:00:00.000Z')); }); }); describe('settle', () => { it('re-anchors at the current effective value without changing it', () => { const { clock, advanceSeconds } = fakeClock('2026-08-21T12:00:00.000Z'); const service = new CharacterVitalsService(clock); const anchor = new Date('2026-08-21T12:00:00.000Z'); const target = character({ currentHp: 40, hpRegenSince: anchor }); advanceSeconds(10); service.settle(target, 100); expect(target.currentHp).toBe(50); expect(target.hpRegenSince).toEqual(new Date('2026-08-21T12:00:10.000Z')); }); it('does not gift overflow past the pre-change maxHp when re-anchoring', () => { const { clock, advanceSeconds } = fakeClock('2026-08-21T12:00:00.000Z'); const service = new CharacterVitalsService(clock); const anchor = new Date('2026-08-21T12:00:00.000Z'); const target = character({ currentHp: 100, hpRegenSince: anchor }); advanceSeconds(600); service.settle(target, 100); expect(target.currentHp).toBe(100); }); }); });