✨Feature: implement combat turn resolution with player and enemy actions
This commit is contained in:
132
src/rules/combatTurns.test.ts
Normal file
132
src/rules/combatTurns.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user