import { type Creature, combatantId, createEncounter, creatureId, isDomainError, } from "@initiative/domain"; import { describe, expect, it } from "vitest"; import { rollAllInitiativeUseCase } from "../roll-all-initiative-use-case.js"; import { expectError, expectSuccess, requireSaved, stubEncounterStore, } from "./helpers.js"; const CREATURE_A = creatureId("creature-a"); const CREATURE_B = creatureId("creature-b"); function makeCreature(id: string, dex = 14): Creature { return { id: creatureId(id), name: `Creature ${id}`, source: "mm", sourceDisplayName: "Monster Manual", size: "Medium", type: "humanoid", alignment: "neutral", ac: 12, hp: { average: 10, formula: "2d8+2" }, speed: "30 ft.", abilities: { str: 10, dex, con: 10, int: 10, wis: 10, cha: 10 }, cr: "1", initiativeProficiency: 0, proficiencyBonus: 2, passive: 10, }; } function encounterWithCombatants( combatants: Array<{ name: string; creatureId?: string; initiative?: number; }>, ) { const result = createEncounter( combatants.map((c) => ({ id: combatantId(c.name), name: c.name, creatureId: c.creatureId ? creatureId(c.creatureId) : undefined, initiative: c.initiative, })), ); if (isDomainError(result)) throw new Error("Setup failed"); return result; } describe("rollAllInitiativeUseCase", () => { it("skips combatants without creatureId", () => { const enc = encounterWithCombatants([ { name: "Fighter" }, { name: "Goblin", creatureId: "creature-a" }, ]); const store = stubEncounterStore(enc); const creature = makeCreature("creature-a"); const result = rollAllInitiativeUseCase( store, () => 10, (id) => (id === CREATURE_A ? creature : undefined), ); expectSuccess(result); expect(result.events.length).toBeGreaterThan(0); const saved = requireSaved(store.saved); const fighter = saved.combatants.find((c) => c.name === "Fighter"); const goblin = saved.combatants.find((c) => c.name === "Goblin"); expect(fighter?.initiative).toBeUndefined(); expect(goblin?.initiative).toBeDefined(); }); it("skips combatants that already have initiative", () => { const enc = encounterWithCombatants([ { name: "Goblin", creatureId: "creature-a", initiative: 15 }, ]); const store = stubEncounterStore(enc); const result = rollAllInitiativeUseCase( store, () => 10, () => makeCreature("creature-a"), ); expectSuccess(result); expect(result.events).toHaveLength(0); expect(requireSaved(store.saved).combatants[0].initiative).toBe(15); }); it("counts skippedNoSource when creature lookup returns undefined", () => { const enc = encounterWithCombatants([ { name: "Unknown", creatureId: "missing" }, ]); const store = stubEncounterStore(enc); const result = rollAllInitiativeUseCase( store, () => 10, () => undefined, ); expectSuccess(result); expect(result.skippedNoSource).toBe(1); expect(result.events).toHaveLength(0); }); it("accumulates events from multiple setInitiative calls", () => { const enc = encounterWithCombatants([ { name: "A", creatureId: "creature-a" }, { name: "B", creatureId: "creature-b" }, ]); const store = stubEncounterStore(enc); const creatureA = makeCreature("creature-a"); const creatureB = makeCreature("creature-b"); const result = rollAllInitiativeUseCase( store, () => 10, (id) => { if (id === CREATURE_A) return creatureA; if (id === CREATURE_B) return creatureB; return undefined; }, ); expectSuccess(result); expect(result.events).toHaveLength(2); }); it("returns early with domain error on invalid dice roll", () => { const enc = encounterWithCombatants([ { name: "A", creatureId: "creature-a" }, { name: "B", creatureId: "creature-b" }, ]); const store = stubEncounterStore(enc); // rollDice returns 0 (invalid — must be 1–20), triggers early return const result = rollAllInitiativeUseCase( store, () => 0, (id) => { if (id === CREATURE_A) return makeCreature("creature-a"); if (id === CREATURE_B) return makeCreature("creature-b"); return undefined; }, ); expectError(result); expect(result.code).toBe("invalid-dice-roll"); // Store should NOT have been saved since the loop aborted expect(store.saved).toBeNull(); }); it("uses higher roll with advantage", () => { const enc = encounterWithCombatants([ { name: "A", creatureId: "creature-a" }, ]); const store = stubEncounterStore(enc); const creature = makeCreature("creature-a"); // Alternating rolls: 5, 15 → advantage picks 15 // Dex 14 → modifier +2, so 15 + 2 = 17 let call = 0; const result = rollAllInitiativeUseCase( store, () => (++call % 2 === 1 ? 5 : 15), (id) => (id === CREATURE_A ? creature : undefined), "advantage", ); expectSuccess(result); expect(requireSaved(store.saved).combatants[0].initiative).toBe(17); }); it("uses lower roll with disadvantage", () => { const enc = encounterWithCombatants([ { name: "A", creatureId: "creature-a" }, ]); const store = stubEncounterStore(enc); const creature = makeCreature("creature-a"); // Alternating rolls: 15, 5 → disadvantage picks 5 // Dex 14 → modifier +2, so 5 + 2 = 7 let call = 0; const result = rollAllInitiativeUseCase( store, () => (++call % 2 === 1 ? 15 : 5), (id) => (id === CREATURE_A ? creature : undefined), "disadvantage", ); expectSuccess(result); expect(requireSaved(store.saved).combatants[0].initiative).toBe(7); }); it("saves encounter once at the end", () => { const enc = encounterWithCombatants([ { name: "A", creatureId: "creature-a" }, { name: "B", creatureId: "creature-b" }, ]); const store = stubEncounterStore(enc); const creatureA = makeCreature("creature-a"); const creatureB = makeCreature("creature-b"); let saveCount = 0; const originalSave = store.save.bind(store); store.save = (e) => { saveCount++; originalSave(e); }; rollAllInitiativeUseCase( store, () => 10, (id) => { if (id === CREATURE_A) return creatureA; if (id === CREATURE_B) return creatureB; return undefined; }, ); expect(saveCount).toBe(1); const saved = requireSaved(store.saved); expect(saved.combatants[0].initiative).toBeDefined(); expect(saved.combatants[1].initiative).toBeDefined(); }); });