Add combat orchestration with turn flow and win/loss check
This commit is contained in:
parent
35d2176bc2
commit
45d62144bf
2 changed files with 125 additions and 0 deletions
65
src/combat.js
Normal file
65
src/combat.js
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { drawCards } from "./state.js";
|
||||||
|
import { resolveEffects } from "./effects.js";
|
||||||
|
import { resolveEnemyAction } from "./enemies.js";
|
||||||
|
import { rollDie } from "./die.js";
|
||||||
|
|
||||||
|
export function startTurn(state) {
|
||||||
|
const dieResult = rollDie();
|
||||||
|
let next = {
|
||||||
|
...state,
|
||||||
|
player: {
|
||||||
|
...state.player,
|
||||||
|
energy: state.player.maxEnergy,
|
||||||
|
block: 0,
|
||||||
|
},
|
||||||
|
combat: {
|
||||||
|
...state.combat,
|
||||||
|
phase: "player_turn",
|
||||||
|
dieResult,
|
||||||
|
selectedCard: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
next = drawCards(next, 5);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveEnemyTurn(state) {
|
||||||
|
let next = {
|
||||||
|
...state,
|
||||||
|
enemy: { ...state.enemy, block: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = resolveEnemyAction(
|
||||||
|
next.enemy,
|
||||||
|
next.combat.dieResult,
|
||||||
|
next.enemy.trackPosition,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (action && action.effects) {
|
||||||
|
next = resolveEffects(next, action.effects, "enemy", "player");
|
||||||
|
}
|
||||||
|
|
||||||
|
let trackPosition = next.enemy.trackPosition;
|
||||||
|
if (next.enemy.actionType === "cube") {
|
||||||
|
trackPosition = Math.min(
|
||||||
|
trackPosition + 1,
|
||||||
|
(next.enemy.actionTrack || []).length - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...next,
|
||||||
|
enemy: { ...next.enemy, trackPosition },
|
||||||
|
combat: {
|
||||||
|
...next.combat,
|
||||||
|
phase: "player_turn",
|
||||||
|
turn: next.combat.turn + 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkCombatEnd(state) {
|
||||||
|
if (state.enemy.hp <= 0) return "victory";
|
||||||
|
if (state.player.hp <= 0) return "defeat";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
60
src/combat.test.js
Normal file
60
src/combat.test.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { startTurn, resolveEnemyTurn, checkCombatEnd } from "./combat.js";
|
||||||
|
import { createCombatState } from "./state.js";
|
||||||
|
|
||||||
|
describe("startTurn", () => {
|
||||||
|
test("resets energy and block, draws 5 cards", () => {
|
||||||
|
let state = createCombatState("ironclad", "jaw_worm");
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
player: { ...state.player, energy: 0, block: 5 },
|
||||||
|
};
|
||||||
|
const next = startTurn(state);
|
||||||
|
expect(next.player.energy).toBe(3);
|
||||||
|
expect(next.player.block).toBe(0);
|
||||||
|
expect(next.player.hand).toHaveLength(5);
|
||||||
|
expect(next.combat.dieResult).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(next.combat.dieResult).toBeLessThanOrEqual(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveEnemyTurn", () => {
|
||||||
|
test("enemy attacks reduce player hp or block", () => {
|
||||||
|
let state = createCombatState("ironclad", "jaw_worm");
|
||||||
|
state = { ...state, combat: { ...state.combat, dieResult: 1 } };
|
||||||
|
const next = resolveEnemyTurn(state);
|
||||||
|
expect(next.combat.phase).toBe("player_turn");
|
||||||
|
expect(next.combat.turn).toBe(state.combat.turn + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("enemy block resets before acting", () => {
|
||||||
|
let state = createCombatState("ironclad", "jaw_worm");
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
enemy: { ...state.enemy, block: 5 },
|
||||||
|
combat: { ...state.combat, dieResult: 1 },
|
||||||
|
};
|
||||||
|
const next = resolveEnemyTurn(state);
|
||||||
|
// die result 1 maps to an attack action (hit 2), so no block regained
|
||||||
|
expect(next.enemy.block).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkCombatEnd", () => {
|
||||||
|
test("returns 'victory' when enemy hp is 0", () => {
|
||||||
|
let state = createCombatState("ironclad", "jaw_worm");
|
||||||
|
state = { ...state, enemy: { ...state.enemy, hp: 0 } };
|
||||||
|
expect(checkCombatEnd(state)).toBe("victory");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 'defeat' when player hp is 0", () => {
|
||||||
|
let state = createCombatState("ironclad", "jaw_worm");
|
||||||
|
state = { ...state, player: { ...state.player, hp: 0 } };
|
||||||
|
expect(checkCombatEnd(state)).toBe("defeat");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null when combat continues", () => {
|
||||||
|
const state = createCombatState("ironclad", "jaw_worm");
|
||||||
|
expect(checkCombatEnd(state)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue