187 lines
5.3 KiB
TypeScript
187 lines
5.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { sampleContentPack } from "@/data/sampleContentPack";
|
|
|
|
import { createStartingAdventurer } from "./character";
|
|
import {
|
|
createRunState,
|
|
enterCurrentRoom,
|
|
resolveRunEnemyTurn,
|
|
resolveRunPlayerTurn,
|
|
startCombatInCurrentRoom,
|
|
} 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,
|
|
);
|
|
});
|
|
});
|