Update combat orchestration for multiplayer turns
startTurn resets energy/block for all players and draws 5 cards each, also resets playersReady. resolveEnemyTurn iterates all enemies, resets each enemy's block, and has each enemy act against player 0. checkCombatEnd checks all enemies (victory) and any player (defeat).
This commit is contained in:
parent
e03b9b2dd7
commit
62425eadd7
4 changed files with 145 additions and 30 deletions
|
|
@ -5,51 +5,76 @@ import { drawCards } from "./state.js";
|
||||||
|
|
||||||
export function startTurn(state) {
|
export function startTurn(state) {
|
||||||
const dieResult = rollDie();
|
const dieResult = rollDie();
|
||||||
|
|
||||||
|
// reset energy and block for all players
|
||||||
|
const players = state.players.map((p) => ({
|
||||||
|
...p,
|
||||||
|
energy: p.maxEnergy,
|
||||||
|
block: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
let next = {
|
let next = {
|
||||||
...state,
|
...state,
|
||||||
player: {
|
players,
|
||||||
...state.player,
|
// keep compat alias in sync
|
||||||
energy: state.player.maxEnergy,
|
player: players[0],
|
||||||
block: 0,
|
|
||||||
},
|
|
||||||
combat: {
|
combat: {
|
||||||
...state.combat,
|
...state.combat,
|
||||||
phase: "player_turn",
|
phase: "player_turn",
|
||||||
dieResult,
|
dieResult,
|
||||||
selectedCard: null,
|
selectedCard: null,
|
||||||
|
playersReady: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
next = drawCards(next, 5);
|
|
||||||
|
// draw 5 cards for each player
|
||||||
|
for (let i = 0; i < next.players.length; i++) {
|
||||||
|
next = drawCards(next, i, 5);
|
||||||
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveEnemyTurn(state) {
|
export function resolveEnemyTurn(state) {
|
||||||
let next = {
|
// reset block on all enemies before they act
|
||||||
...state,
|
const enemies = state.enemies.map((e) => ({ ...e, block: 0 }));
|
||||||
enemy: { ...state.enemy, block: 0 },
|
let next = { ...state, enemies, enemy: enemies[0] };
|
||||||
};
|
|
||||||
|
|
||||||
const action = resolveEnemyAction(
|
// each enemy acts; enemies target player 0 by default
|
||||||
next.enemy,
|
for (let i = 0; i < next.enemies.length; i++) {
|
||||||
next.combat.dieResult,
|
const enemy = next.enemies[i];
|
||||||
next.enemy.trackPosition,
|
const action = resolveEnemyAction(
|
||||||
);
|
enemy,
|
||||||
|
next.combat.dieResult,
|
||||||
if (action?.effects) {
|
enemy.trackPosition,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (action?.effects) {
|
||||||
|
// TODO: implement enemy targeting logic for multiplayer (random, lowest HP, etc.)
|
||||||
|
next = resolveEffects(
|
||||||
|
next,
|
||||||
|
action.effects,
|
||||||
|
{ type: "enemy", index: i },
|
||||||
|
{ type: "player", index: 0 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// advance cube track for this enemy if applicable
|
||||||
|
if (enemy.actionType === "cube") {
|
||||||
|
const trackPosition = Math.min(
|
||||||
|
enemy.trackPosition + 1,
|
||||||
|
(enemy.actionTrack || []).length - 1,
|
||||||
|
);
|
||||||
|
const updatedEnemy = { ...next.enemies[i], trackPosition };
|
||||||
|
const updatedEnemies = next.enemies.map((e, j) =>
|
||||||
|
j === i ? updatedEnemy : e,
|
||||||
|
);
|
||||||
|
next = { ...next, enemies: updatedEnemies, enemy: updatedEnemies[0] };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...next,
|
...next,
|
||||||
enemy: { ...next.enemy, trackPosition },
|
|
||||||
combat: {
|
combat: {
|
||||||
...next.combat,
|
...next.combat,
|
||||||
phase: "player_turn",
|
phase: "player_turn",
|
||||||
|
|
@ -59,7 +84,7 @@ export function resolveEnemyTurn(state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkCombatEnd(state) {
|
export function checkCombatEnd(state) {
|
||||||
if (state.enemy.hp <= 0) return "victory";
|
if (state.enemies.every((e) => e.hp <= 0)) return "victory";
|
||||||
if (state.player.hp <= 0) return "defeat";
|
if (state.players.some((p) => p.hp <= 0)) return "defeat";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,13 +49,15 @@ describe("resolveEnemyTurn", () => {
|
||||||
describe("checkCombatEnd", () => {
|
describe("checkCombatEnd", () => {
|
||||||
test("returns 'victory' when enemy hp is 0", () => {
|
test("returns 'victory' when enemy hp is 0", () => {
|
||||||
let state = createCombatState("ironclad", "jaw_worm");
|
let state = createCombatState("ironclad", "jaw_worm");
|
||||||
state = { ...state, enemy: { ...state.enemy, hp: 0 } };
|
const deadEnemy = { ...state.enemies[0], hp: 0 };
|
||||||
|
state = { ...state, enemy: deadEnemy, enemies: [deadEnemy] };
|
||||||
expect(checkCombatEnd(state)).toBe("victory");
|
expect(checkCombatEnd(state)).toBe("victory");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns 'defeat' when player hp is 0", () => {
|
test("returns 'defeat' when player hp is 0", () => {
|
||||||
let state = createCombatState("ironclad", "jaw_worm");
|
let state = createCombatState("ironclad", "jaw_worm");
|
||||||
state = { ...state, player: { ...state.player, hp: 0 } };
|
const deadPlayer = { ...state.players[0], hp: 0 };
|
||||||
|
state = { ...state, player: deadPlayer, players: [deadPlayer] };
|
||||||
expect(checkCombatEnd(state)).toBe("defeat");
|
expect(checkCombatEnd(state)).toBe("defeat");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -64,3 +66,81 @@ describe("checkCombatEnd", () => {
|
||||||
expect(checkCombatEnd(state)).toBeNull();
|
expect(checkCombatEnd(state)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("startTurn - multiplayer", () => {
|
||||||
|
test("resets energy and block for all players, draws 5 each", () => {
|
||||||
|
let state = createCombatState(["ironclad", "ironclad"], ["jaw_worm"]);
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
players: state.players.map((p) => ({ ...p, energy: 0, block: 5 })),
|
||||||
|
};
|
||||||
|
const next = startTurn(state);
|
||||||
|
expect(next.players[0].energy).toBe(3);
|
||||||
|
expect(next.players[0].block).toBe(0);
|
||||||
|
expect(next.players[0].hand).toHaveLength(5);
|
||||||
|
expect(next.players[1].energy).toBe(3);
|
||||||
|
expect(next.players[1].block).toBe(0);
|
||||||
|
expect(next.players[1].hand).toHaveLength(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("playersReady resets on startTurn", () => {
|
||||||
|
let state = createCombatState(["ironclad", "ironclad"], ["jaw_worm"]);
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
combat: { ...state.combat, playersReady: [0, 1] },
|
||||||
|
};
|
||||||
|
const next = startTurn(state);
|
||||||
|
expect(next.combat.playersReady).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveEnemyTurn - multiplayer", () => {
|
||||||
|
test("all enemies act, block resets for each", () => {
|
||||||
|
let state = createCombatState(["ironclad"], ["jaw_worm", "jaw_worm"]);
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
enemies: state.enemies.map((e) => ({ ...e, block: 3 })),
|
||||||
|
combat: { ...state.combat, dieResult: 1 },
|
||||||
|
};
|
||||||
|
const next = resolveEnemyTurn(state);
|
||||||
|
// die result 1 is defend (3 hit + 1 block), so block resets then gains 1
|
||||||
|
expect(next.enemies[0].block).toBe(1);
|
||||||
|
expect(next.enemies[1].block).toBe(1);
|
||||||
|
expect(next.combat.turn).toBe(state.combat.turn + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkCombatEnd - multiplayer", () => {
|
||||||
|
test("victory when all enemies at 0 hp", () => {
|
||||||
|
let state = createCombatState(["ironclad"], ["jaw_worm", "jaw_worm"]);
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
enemies: state.enemies.map((e) => ({ ...e, hp: 0 })),
|
||||||
|
};
|
||||||
|
expect(checkCombatEnd(state)).toBe("victory");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no victory if only one enemy at 0 hp", () => {
|
||||||
|
let state = createCombatState(["ironclad"], ["jaw_worm", "jaw_worm"]);
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
enemies: [
|
||||||
|
{ ...state.enemies[0], hp: 0 },
|
||||||
|
{ ...state.enemies[1], hp: 5 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(checkCombatEnd(state)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("defeat when any player at 0 hp", () => {
|
||||||
|
let state = createCombatState(["ironclad", "ironclad"], ["jaw_worm"]);
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
players: [
|
||||||
|
{ ...state.players[0], hp: 0 },
|
||||||
|
{ ...state.players[1], hp: 11 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(checkCombatEnd(state)).toBe("defeat");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,8 @@ export function endTurn(state, playerIndex) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.combat.playersReady.includes(playerIndex)) return state;
|
||||||
|
|
||||||
const player = state.players[playerIndex];
|
const player = state.players[playerIndex];
|
||||||
const updatedPlayer = {
|
const updatedPlayer = {
|
||||||
...player,
|
...player,
|
||||||
|
|
|
||||||
|
|
@ -217,4 +217,12 @@ describe("endTurn - indexed player", () => {
|
||||||
expect(next.combat.phase).toBe("enemy_turn");
|
expect(next.combat.phase).toBe("enemy_turn");
|
||||||
expect(next.combat.playersReady).toHaveLength(2);
|
expect(next.combat.playersReady).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("calling endTurn twice for the same player does not add duplicate to playersReady", () => {
|
||||||
|
let state = createCombatState(["ironclad", "ironclad"], ["jaw_worm"]);
|
||||||
|
state = endTurn(state, 0);
|
||||||
|
const next = endTurn(state, 0);
|
||||||
|
expect(next.combat.playersReady).toHaveLength(1);
|
||||||
|
expect(next.combat.phase).toBe("player_turn");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue