import { describe, expect, it } from "vitest"; import { sampleContentPack } from "@/data/sampleContentPack"; import { createStartingAdventurer } from "./character"; import { startCombatFromRoom } from "./combat"; import { resolveEnemyTurn, resolvePlayerAttack } from "./combatTurns"; import { createRoomStateFromTemplate } from "./rooms"; function createSequenceRoller(values: number[]) { let index = 0; return () => { const next = values[index]; index += 1; return next; }; } function createGuardCombat() { const adventurer = createStartingAdventurer(sampleContentPack, { name: "Aster", weaponId: "weapon.short-sword", armourId: "armour.leather-vest", scrollId: "scroll.lesser-heal", }); const room = createRoomStateFromTemplate( sampleContentPack, "room.level1.guard-room", 1, "room.level1.normal.guard-room", ); room.encounter = { id: `${room.id}.encounter`, sourceTableCode: "L1G", creatureIds: ["a", "b"], creatureNames: ["Guard", "Warrior"], resultLabel: "Guard and Warrior", resolved: true, }; return { adventurer, ...startCombatFromRoom({ content: sampleContentPack, adventurer, room, at: "2026-03-15T13:00:00.000Z", }), }; } describe("combat turns", () => { it("lets the player hit an enemy and pass initiative", () => { const { adventurer, combat } = createGuardCombat(); const targetId = combat.enemies[0]!.id; const result = resolvePlayerAttack({ content: sampleContentPack, combat, adventurer, manoeuvreId: "manoeuvre.exact-strike", targetEnemyId: targetId, roller: createSequenceRoller([6, 6]), at: "2026-03-15T13:01:00.000Z", }); expect(result.combat.actingSide).toBe("enemy"); expect(result.combat.selectedManoeuvreId).toBe("manoeuvre.exact-strike"); expect(result.combat.enemies[0]?.hpCurrent).toBeLessThan(result.combat.enemies[0]!.hpMax); expect(result.logEntries[0]?.text).toContain("deals"); }); it("marks an enemy defeated when the player deals lethal damage", () => { const { adventurer, combat } = createGuardCombat(); combat.enemies[0]!.hpCurrent = 1; const result = resolvePlayerAttack({ content: sampleContentPack, combat, adventurer, manoeuvreId: "manoeuvre.guard-break", targetEnemyId: combat.enemies[0]!.id, roller: createSequenceRoller([6, 5]), at: "2026-03-15T13:02:00.000Z", }); expect(result.defeatedEnemyIds).toEqual([combat.enemies[0]!.id]); expect(result.logEntries.map((entry) => entry.text)).toContain("Guard is defeated."); }); it("lets an enemy attack the player and advances the round", () => { const { adventurer, combat } = createGuardCombat(); const afterPlayer = resolvePlayerAttack({ content: sampleContentPack, combat, adventurer, manoeuvreId: "manoeuvre.exact-strike", targetEnemyId: combat.enemies[0]!.id, roller: createSequenceRoller([4, 4]), at: "2026-03-15T13:03:00.000Z", }); const result = resolveEnemyTurn({ content: sampleContentPack, combat: afterPlayer.combat, adventurer, roller: createSequenceRoller([6, 6]), at: "2026-03-15T13:04:00.000Z", }); expect(result.combat.round).toBe(2); expect(result.combat.actingSide).toBe("player"); expect(result.combat.player.hpCurrent).toBeLessThan(result.combat.player.hpMax); expect(result.logEntries[0]?.text).toContain("attacks"); }); it("rejects player turns that target a defeated enemy", () => { const { adventurer, combat } = createGuardCombat(); combat.enemies[0]!.hpCurrent = 0; expect(() => resolvePlayerAttack({ content: sampleContentPack, combat, adventurer, manoeuvreId: "manoeuvre.exact-strike", targetEnemyId: combat.enemies[0]!.id, }), ).toThrow("Unknown or defeated target"); }); });