import { describe, expect, it } from "vitest"; import { sampleContentPack } from "@/data/sampleContentPack"; import { createStartingAdventurer } from "./character"; import { createRunState, enterCurrentRoom, getAvailableMoves, resolveRunEnemyTurn, resolveRunPlayerTurn, startCombatInCurrentRoom, travelCurrentExit, } from "./runState"; function createSequenceRoller(values: number[]) { let index = 0; return () => { const next = values[index]; index += 1; return next; }; } function createAdventurer() { return createStartingAdventurer(sampleContentPack, { name: "Aster", weaponId: "weapon.short-sword", armourId: "armour.leather-vest", scrollId: "scroll.lesser-heal", }); } describe("run state flow", () => { it("creates a run anchored in the start room", () => { const run = createRunState({ content: sampleContentPack, campaignId: "campaign.1", adventurer: createAdventurer(), at: "2026-03-15T14:00:00.000Z", }); expect(run.currentLevel).toBe(1); expect(run.currentRoomId).toBe("room.level1.start"); expect(run.dungeon.levels["1"]?.rooms["room.level1.start"]).toBeDefined(); }); it("enters the current room and appends room logs", () => { const run = createRunState({ content: sampleContentPack, campaignId: "campaign.1", adventurer: createAdventurer(), at: "2026-03-15T14:00:00.000Z", }); const result = enterCurrentRoom({ content: sampleContentPack, run, at: "2026-03-15T14:01:00.000Z", }); expect(result.run.log).toHaveLength(1); expect(result.run.log[0]?.text).toContain("Re-entered Entry Chamber"); }); it("starts combat from the current room and stores the active combat state", () => { const run = createRunState({ content: sampleContentPack, campaignId: "campaign.1", adventurer: createAdventurer(), at: "2026-03-15T14:00:00.000Z", }); const levelState = run.dungeon.levels["1"]!; const room = levelState.rooms["room.level1.start"]!; room.encounter = { id: `${room.id}.encounter`, sourceTableCode: "L1G", creatureIds: ["a", "b"], creatureNames: ["Guard", "Warrior"], resultLabel: "Guard and Warrior", resolved: true, }; const result = startCombatInCurrentRoom({ content: sampleContentPack, run, at: "2026-03-15T14:02:00.000Z", }); expect(result.run.activeCombat?.enemies).toHaveLength(2); expect(result.run.log.at(-1)?.text).toContain("Combat begins"); }); it("resolves player and enemy turns through run state", () => { const run = createRunState({ content: sampleContentPack, campaignId: "campaign.1", adventurer: createAdventurer(), at: "2026-03-15T14:00:00.000Z", }); const levelState = run.dungeon.levels["1"]!; const room = levelState.rooms["room.level1.start"]!; room.encounter = { id: `${room.id}.encounter`, sourceTableCode: "L1G", creatureIds: ["a", "b"], creatureNames: ["Guard", "Warrior"], resultLabel: "Guard and Warrior", resolved: true, }; const withCombat = startCombatInCurrentRoom({ content: sampleContentPack, run, at: "2026-03-15T14:02:00.000Z", }).run; const targetEnemyId = withCombat.activeCombat!.enemies[0]!.id; const afterPlayer = resolveRunPlayerTurn({ content: sampleContentPack, run: withCombat, manoeuvreId: "manoeuvre.exact-strike", targetEnemyId, roller: createSequenceRoller([6, 6]), at: "2026-03-15T14:03:00.000Z", }).run; expect(afterPlayer.activeCombat?.actingSide).toBe("enemy"); expect(afterPlayer.log.at(-1)?.text).toContain("deals"); const afterEnemy = resolveRunEnemyTurn({ content: sampleContentPack, run: afterPlayer, roller: createSequenceRoller([6, 6]), at: "2026-03-15T14:04:00.000Z", }).run; expect(afterEnemy.activeCombat?.actingSide).toBe("player"); expect(afterEnemy.adventurerSnapshot.hp.current).toBeLessThan( createAdventurer().hp.current, ); }); it("clears the room and ends active combat when the last enemy falls", () => { const run = createRunState({ content: sampleContentPack, campaignId: "campaign.1", adventurer: createAdventurer(), at: "2026-03-15T14:00:00.000Z", }); const room = run.dungeon.levels["1"]!.rooms["room.level1.start"]!; room.encounter = { id: `${room.id}.encounter`, sourceTableCode: "L1G", creatureIds: ["a"], creatureNames: ["Guard"], resultLabel: "Guard", resolved: true, }; const withCombat = startCombatInCurrentRoom({ content: sampleContentPack, run, at: "2026-03-15T14:02:00.000Z", }).run; withCombat.activeCombat!.enemies[0]!.hpCurrent = 1; const result = resolveRunPlayerTurn({ content: sampleContentPack, run: withCombat, manoeuvreId: "manoeuvre.guard-break", targetEnemyId: withCombat.activeCombat!.enemies[0]!.id, roller: createSequenceRoller([6, 6]), at: "2026-03-15T14:03:00.000Z", }); expect(result.run.activeCombat).toBeUndefined(); expect(result.run.dungeon.levels["1"]!.rooms["room.level1.start"]!.discovery.cleared).toBe(true); expect(result.run.dungeon.levels["1"]!.rooms["room.level1.start"]!.encounter?.rewardPending).toBe( false, ); }); it("lists available traversable exits for the current room", () => { const run = createRunState({ content: sampleContentPack, campaignId: "campaign.1", adventurer: createAdventurer(), }); expect(getAvailableMoves(run)).toEqual([ expect.objectContaining({ direction: "north", generated: false, }), ]); }); it("travels through an unresolved exit, generates a room, and enters it", () => { const run = createRunState({ content: sampleContentPack, campaignId: "campaign.1", adventurer: createAdventurer(), at: "2026-03-15T14:00:00.000Z", }); const result = travelCurrentExit({ content: sampleContentPack, run, exitDirection: "north", roller: createSequenceRoller([1, 1]), at: "2026-03-15T14:05:00.000Z", }); expect(result.run.currentRoomId).toBe("room.level1.room.002"); expect(result.run.dungeon.levels["1"]!.discoveredRoomOrder).toEqual([ "room.level1.start", "room.level1.room.002", ]); expect(result.run.dungeon.levels["1"]!.rooms["room.level1.room.002"]!.discovery.entered).toBe( true, ); expect(result.run.log[0]?.text).toContain("Travelled north"); }); });