137 lines
4.6 KiB
TypeScript
137 lines
4.6 KiB
TypeScript
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> = {}): 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);
|
|
});
|
|
});
|
|
});
|