133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
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");
|
|
});
|
|
});
|