Tests verify the get→call→save wiring and error propagation for each use case. The 15 formulaic use cases share a test file; rollInitiative and rollAllInitiative have dedicated suites covering their multi-step logic (creature lookup, modifier calculation, iteration, early return). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
156 lines
4.1 KiB
TypeScript
156 lines
4.1 KiB
TypeScript
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>): 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);
|
|
});
|
|
});
|