import { type Creature, type CreatureId, combatantId, createEncounter, creatureId, isDomainError, } from "@initiative/domain"; import { describe, expect, it } from "vitest"; import { addCombatantUseCase } from "../add-combatant-use-case.js"; import { rollInitiativeUseCase } from "../roll-initiative-use-case.js"; import { expectError, requireSaved, stubEncounterStore } from "./helpers.js"; const GOBLIN_ID = creatureId("goblin"); function makeCreature(overrides?: Partial): Creature { return { id: GOBLIN_ID, name: "Goblin", source: "mm", sourceDisplayName: "Monster Manual", size: "Small", type: "humanoid", alignment: "neutral evil", ac: 15, hp: { average: 7, formula: "2d6" }, speed: "30 ft.", abilities: { str: 8, dex: 14, con: 10, int: 10, wis: 8, cha: 8 }, cr: "1/4", initiativeProficiency: 0, proficiencyBonus: 2, passive: 9, ...overrides, }; } function encounterWithCreatureLink(name: string, creature: CreatureId) { const enc = createEncounter([]); if (isDomainError(enc)) throw new Error("Setup failed"); const id = combatantId(name); const store = stubEncounterStore(enc); addCombatantUseCase(store, id, name); const saved = requireSaved(store.saved); const result = createEncounter( saved.combatants.map((c) => c.id === id ? { ...c, creatureId: creature } : c, ), saved.activeIndex, saved.roundNumber, ); if (isDomainError(result)) throw new Error("Setup failed"); return result; } describe("rollInitiativeUseCase", () => { it("returns domain error when combatant not found", () => { const enc = createEncounter([]); if (isDomainError(enc)) throw new Error("Setup failed"); const store = stubEncounterStore(enc); const result = rollInitiativeUseCase( store, combatantId("unknown"), 10, () => undefined, ); expectError(result); expect(result.code).toBe("combatant-not-found"); expect(store.saved).toBeNull(); }); it("returns domain error when combatant has no creature link", () => { const enc = createEncounter([]); if (isDomainError(enc)) throw new Error("Setup failed"); const store1 = stubEncounterStore(enc); addCombatantUseCase(store1, combatantId("Fighter"), "Fighter"); const store = stubEncounterStore(requireSaved(store1.saved)); const result = rollInitiativeUseCase( store, combatantId("Fighter"), 10, () => undefined, ); expectError(result); expect(result.code).toBe("no-creature-link"); expect(store.saved).toBeNull(); }); it("returns domain error when creature not found in getter", () => { const enc = encounterWithCreatureLink("Goblin", GOBLIN_ID); const store = stubEncounterStore(enc); const result = rollInitiativeUseCase( store, combatantId("Goblin"), 10, () => undefined, ); expectError(result); expect(result.code).toBe("creature-not-found"); expect(store.saved).toBeNull(); }); it("calculates initiative from creature and saves", () => { const creature = makeCreature(); const enc = encounterWithCreatureLink("Goblin", GOBLIN_ID); const store = stubEncounterStore(enc); // Dex 14 -> modifier +2, CR 1/4 -> PB 2, initiativeProficiency 0 // So initiative modifier = 2 + 0*2 = 2 // Roll 10 + modifier 2 = 12 const result = rollInitiativeUseCase( store, combatantId("Goblin"), 10, (id) => (id === GOBLIN_ID ? creature : undefined), ); expect(isDomainError(result)).toBe(false); expect(requireSaved(store.saved).combatants[0].initiative).toBe(12); }); it("applies initiative proficiency bonus correctly", () => { // CR 5 -> PB 3, dex 16 -> mod +3, initiativeProficiency 1 // modifier = 3 + 1*3 = 6, roll 8 + 6 = 14 const creature = makeCreature({ abilities: { str: 10, dex: 16, con: 10, int: 10, wis: 10, cha: 10, }, cr: "5", initiativeProficiency: 1, }); const enc = encounterWithCreatureLink("Monster", GOBLIN_ID); const store = stubEncounterStore(enc); const result = rollInitiativeUseCase( store, combatantId("Monster"), 8, (id) => (id === GOBLIN_ID ? creature : undefined), ); expect(isDomainError(result)).toBe(false); expect(requireSaved(store.saved).combatants[0].initiative).toBe(14); }); });