import { describe, expect, it } from "vitest"; import type { HpRange, HpVariant } from "../hp-range.js"; import { setHpVariant } from "../set-hp-variant.js"; import type { Combatant, Encounter } from "../types.js"; import { combatantId, isDomainError } from "../types.js"; import { expectDomainError } from "./test-helpers.js"; // A 3d6 + 6 creature: 9 / 16 / 24. const RANGE: HpRange = { min: 9, average: 16, max: 24 }; function makeCombatant(opts?: Partial): Combatant { return { id: combatantId("c-1"), name: "Ogre", maxHp: 16, currentHp: 16, ...opts, }; } function enc(combatants: Combatant[]): Encounter { return { combatants, activeIndex: 0, roundNumber: 1 }; } function apply( encounter: Encounter, variant: HpVariant | undefined, range: HpRange = RANGE, ) { const result = setHpVariant(encounter, combatantId("c-1"), variant, range); if (isDomainError(result)) { throw new Error(`Expected success, got error: ${result.message}`); } return result; } describe("setHpVariant", () => { describe("acceptance scenarios", () => { it("average → max raises maxHp and currentHp to the maximum roll", () => { const { encounter } = apply(enc([makeCombatant()]), "max"); const c = encounter.combatants[0]; expect(c.maxHp).toBe(24); expect(c.currentHp).toBe(24); expect(c.hpVariant).toBe("max"); }); it("average → min lowers maxHp and currentHp to the minimum roll", () => { const { encounter } = apply(enc([makeCombatant()]), "min"); const c = encounter.combatants[0]; expect(c.maxHp).toBe(9); expect(c.currentHp).toBe(9); expect(c.hpVariant).toBe("min"); }); it("max → average restores the printed average", () => { const start = enc([ makeCombatant({ maxHp: 24, currentHp: 24, hpVariant: "max" }), ]); const { encounter } = apply(start, undefined); const c = encounter.combatants[0]; expect(c.maxHp).toBe(16); expect(c.currentHp).toBe(16); expect(c.hpVariant).toBeUndefined(); }); it("min → max spans the full range in one step", () => { const start = enc([ makeCombatant({ maxHp: 9, currentHp: 9, hpVariant: "min" }), ]); const { encounter } = apply(start, "max"); expect(encounter.combatants[0].maxHp).toBe(24); }); }); describe("damage already taken", () => { it("keeps the amount of damage taken when raising max HP", () => { // 5 damage taken: 11/16 → 19/24 const start = enc([makeCombatant({ maxHp: 16, currentHp: 11 })]); const { encounter } = apply(start, "max"); const c = encounter.combatants[0]; expect(c.maxHp).toBe(24); expect(c.currentHp).toBe(19); }); it("floors currentHp at 0 when lowering max HP past the damage taken", () => { const start = enc([makeCombatant({ maxHp: 16, currentHp: 3 })]); const { encounter } = apply(start, "min"); const c = encounter.combatants[0]; expect(c.maxHp).toBe(9); expect(c.currentHp).toBe(0); }); it("clamps currentHp to the new maxHp", () => { const start = enc([makeCombatant({ maxHp: 100, currentHp: 100 })]); const { encounter } = apply(start, "min"); const c = encounter.combatants[0]; expect(c.maxHp).toBe(93); expect(c.currentHp).toBe(93); }); }); describe("edge cases", () => { it("preserves a manual max HP edit as an offset", () => { // User bumped the ogre to 20 HP; max is 8 above average. const start = enc([makeCombatant({ maxHp: 20, currentHp: 20 })]); const { encounter } = apply(start, "max"); expect(encounter.combatants[0].maxHp).toBe(28); }); it("never lets maxHp drop below 1", () => { const start = enc([makeCombatant({ maxHp: 2, currentHp: 2 })]); const { encounter } = apply(start, "min"); expect(encounter.combatants[0].maxHp).toBe(1); }); it("leaves a combatant without HP untouched but records the variant", () => { const start = enc([ makeCombatant({ maxHp: undefined, currentHp: undefined }), ]); const { encounter } = apply(start, "max"); const c = encounter.combatants[0]; expect(c.maxHp).toBeUndefined(); expect(c.currentHp).toBeUndefined(); expect(c.hpVariant).toBe("max"); }); it("is a no-op when the variant is already active", () => { const start = enc([ makeCombatant({ maxHp: 24, currentHp: 20, hpVariant: "max" }), ]); const { encounter, events } = apply(start, "max"); expect(encounter.combatants[0].currentHp).toBe(20); expect(events).toEqual([]); }); it("returns a domain error for an unknown combatant", () => { const result = setHpVariant( enc([makeCombatant()]), combatantId("c-99"), "max", RANGE, ); expectDomainError(result, "combatant-not-found"); }); }); it("emits an HpVariantSet event carrying the HP change", () => { const { events } = apply(enc([makeCombatant()]), "max"); expect(events).toEqual([ { type: "HpVariantSet", combatantId: combatantId("c-1"), variant: "max", previousMaxHp: 16, newMaxHp: 24, }, ]); }); });