Add combat orchestration with turn flow and win/loss check

This commit is contained in:
Jared Miller 2026-02-23 17:38:02 -05:00
parent 35d2176bc2
commit 45d62144bf
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 125 additions and 0 deletions

65
src/combat.js Normal file
View 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
View 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();
});
});