279 lines
6.9 KiB
JavaScript
279 lines
6.9 KiB
JavaScript
import { getCard, getStarterDeck } from "./cards.js";
|
|
import { resolveEffects } from "./effects.js";
|
|
import { getEnemy } from "./enemies.js";
|
|
|
|
function makePlayer(character, index) {
|
|
return {
|
|
id: `player_${index}`,
|
|
character,
|
|
hp: 11,
|
|
maxHp: 11,
|
|
energy: 3,
|
|
maxEnergy: 3,
|
|
block: 0,
|
|
strength: 0,
|
|
vulnerable: 0,
|
|
weak: 0,
|
|
drawPile: shuffle([...getStarterDeck(character)]),
|
|
hand: [],
|
|
discardPile: [],
|
|
exhaustPile: [],
|
|
powers: [],
|
|
};
|
|
}
|
|
|
|
function makeEnemy(enemyId, index) {
|
|
const enemy = getEnemy(enemyId);
|
|
return {
|
|
id: enemy.id,
|
|
instanceId: `${enemy.id}_${index}`,
|
|
name: enemy.name,
|
|
hp: enemy.hp,
|
|
maxHp: enemy.hp,
|
|
block: 0,
|
|
strength: 0,
|
|
vulnerable: 0,
|
|
weak: 0,
|
|
actionType: enemy.actionType,
|
|
actions: enemy.actions,
|
|
actionTrack: enemy.actionTrack || null,
|
|
trackPosition: 0,
|
|
row: index,
|
|
};
|
|
}
|
|
|
|
export function createCombatState(characterOrChars, enemyIdOrIds) {
|
|
const characters = Array.isArray(characterOrChars)
|
|
? characterOrChars
|
|
: [characterOrChars];
|
|
const enemyIds = Array.isArray(enemyIdOrIds) ? enemyIdOrIds : [enemyIdOrIds];
|
|
|
|
const players = characters.map((c, i) => makePlayer(c, i));
|
|
const enemies = enemyIds.map((id, i) => makeEnemy(id, i));
|
|
|
|
const state = {
|
|
players,
|
|
enemies,
|
|
combat: {
|
|
turn: 1,
|
|
phase: "player_turn",
|
|
dieResult: null,
|
|
selectedCard: null,
|
|
log: [],
|
|
playerCount: players.length,
|
|
activePlayerIndex: null, // TODO: unused — remove once no callers remain
|
|
playersReady: [],
|
|
},
|
|
};
|
|
|
|
// backward-compat aliases — only valid for single-enemy encounters
|
|
state.player = players[0];
|
|
state.enemy = enemies[0];
|
|
|
|
return state;
|
|
}
|
|
|
|
export function createCombatFromRun(run, enemyIdOrIds) {
|
|
const enemyIds = Array.isArray(enemyIdOrIds) ? enemyIdOrIds : [enemyIdOrIds];
|
|
const enemies = enemyIds.map((id, i) => makeEnemy(id, i));
|
|
|
|
const player = {
|
|
id: "player_0",
|
|
character: run.character,
|
|
hp: run.hp,
|
|
maxHp: run.maxHp,
|
|
energy: 3,
|
|
maxEnergy: 3,
|
|
block: 0,
|
|
strength: 0,
|
|
vulnerable: 0,
|
|
weak: 0,
|
|
drawPile: shuffle([...run.deck]),
|
|
hand: [],
|
|
discardPile: [],
|
|
exhaustPile: [],
|
|
powers: [],
|
|
};
|
|
|
|
const state = {
|
|
players: [player],
|
|
enemies,
|
|
combat: {
|
|
turn: 1,
|
|
phase: "player_turn",
|
|
dieResult: null,
|
|
selectedCard: null,
|
|
log: [],
|
|
playerCount: 1,
|
|
activePlayerIndex: null,
|
|
playersReady: [],
|
|
},
|
|
};
|
|
|
|
state.player = player;
|
|
state.enemy = enemies[0];
|
|
return state;
|
|
}
|
|
|
|
export function drawCards(state, playerIndexOrCount, count) {
|
|
// drawCards(state, count) — old single-player form
|
|
// drawCards(state, playerIndex, count) — new indexed form
|
|
let playerIndex, drawCount;
|
|
if (count === undefined) {
|
|
playerIndex = 0;
|
|
drawCount = playerIndexOrCount;
|
|
} else {
|
|
playerIndex = playerIndexOrCount;
|
|
drawCount = count;
|
|
}
|
|
|
|
const player = state.players[playerIndex];
|
|
let drawPile = [...player.drawPile];
|
|
let discardPile = [...player.discardPile];
|
|
const hand = [...player.hand];
|
|
|
|
for (let i = 0; i < drawCount; i++) {
|
|
if (drawPile.length === 0) {
|
|
drawPile = shuffle(discardPile);
|
|
discardPile = [];
|
|
}
|
|
if (drawPile.length > 0) {
|
|
hand.push(drawPile.pop());
|
|
}
|
|
}
|
|
|
|
const updatedPlayer = { ...player, drawPile, hand, discardPile };
|
|
const players = state.players.map((p, i) =>
|
|
i === playerIndex ? updatedPlayer : p,
|
|
);
|
|
|
|
return {
|
|
...state,
|
|
players,
|
|
// keep top-level compat alias in sync
|
|
player: players[0],
|
|
};
|
|
}
|
|
|
|
export function playCard(state, handIndex, opts = {}) {
|
|
const playerIndex = opts.playerIndex ?? 0;
|
|
const enemyIndex = opts.targetIndex ?? 0;
|
|
|
|
const player = state.players[playerIndex];
|
|
const cardId = player.hand[handIndex];
|
|
const card = getCard(cardId);
|
|
if (card.keywords?.includes("unplayable")) return null;
|
|
if (player.energy < card.cost) return null;
|
|
|
|
const hand = [...player.hand];
|
|
hand.splice(handIndex, 1);
|
|
const exhausted = card.keywords?.includes("exhaust");
|
|
const discardPile = exhausted
|
|
? [...player.discardPile]
|
|
: [...player.discardPile, cardId];
|
|
const exhaustPile = exhausted
|
|
? [...player.exhaustPile, cardId]
|
|
: [...player.exhaustPile];
|
|
const energy = player.energy - card.cost;
|
|
|
|
const updatedPlayer = { ...player, hand, discardPile, exhaustPile, energy };
|
|
const players = state.players.map((p, i) =>
|
|
i === playerIndex ? updatedPlayer : p,
|
|
);
|
|
|
|
let next = {
|
|
...state,
|
|
players,
|
|
player: players[0],
|
|
};
|
|
|
|
next = resolveEffects(
|
|
next,
|
|
card.effects,
|
|
{ type: "player", index: playerIndex },
|
|
{ type: "enemy", index: enemyIndex },
|
|
);
|
|
return next;
|
|
}
|
|
|
|
export function endTurn(state, playerIndex) {
|
|
// endTurn(state) — old form: discards all, sets enemy phase
|
|
// endTurn(state, playerIndex) — new form: marks one player done
|
|
if (playerIndex === undefined) {
|
|
const player = state.players[0];
|
|
const retained = [];
|
|
const exhausted = [];
|
|
const discarded = [];
|
|
for (const cardId of player.hand) {
|
|
const card = getCard(cardId);
|
|
if (card.keywords?.includes("retain")) {
|
|
retained.push(cardId);
|
|
} else if (card.keywords?.includes("ethereal")) {
|
|
exhausted.push(cardId);
|
|
} else {
|
|
discarded.push(cardId);
|
|
}
|
|
}
|
|
const updatedPlayer = {
|
|
...player,
|
|
hand: retained,
|
|
discardPile: [...player.discardPile, ...discarded],
|
|
exhaustPile: [...player.exhaustPile, ...exhausted],
|
|
};
|
|
const players = state.players.map((p, i) => (i === 0 ? updatedPlayer : p));
|
|
return {
|
|
...state,
|
|
players,
|
|
player: players[0],
|
|
combat: { ...state.combat, phase: "enemy_turn" },
|
|
};
|
|
}
|
|
|
|
if (state.combat.playersReady.includes(playerIndex)) return state;
|
|
|
|
const player = state.players[playerIndex];
|
|
const retained = [];
|
|
const exhausted = [];
|
|
const discarded = [];
|
|
for (const cardId of player.hand) {
|
|
const card = getCard(cardId);
|
|
if (card.keywords?.includes("retain")) {
|
|
retained.push(cardId);
|
|
} else if (card.keywords?.includes("ethereal")) {
|
|
exhausted.push(cardId);
|
|
} else {
|
|
discarded.push(cardId);
|
|
}
|
|
}
|
|
const updatedPlayer = {
|
|
...player,
|
|
hand: retained,
|
|
discardPile: [...player.discardPile, ...discarded],
|
|
exhaustPile: [...player.exhaustPile, ...exhausted],
|
|
};
|
|
const players = state.players.map((p, i) =>
|
|
i === playerIndex ? updatedPlayer : p,
|
|
);
|
|
const playersReady = [...state.combat.playersReady, playerIndex];
|
|
const allReady = playersReady.length >= state.combat.playerCount;
|
|
|
|
return {
|
|
...state,
|
|
players,
|
|
player: players[0],
|
|
combat: {
|
|
...state.combat,
|
|
playersReady,
|
|
phase: allReady ? "enemy_turn" : state.combat.phase,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function shuffle(arr) {
|
|
const a = [...arr];
|
|
for (let i = a.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[a[i], a[j]] = [a[j], a[i]];
|
|
}
|
|
return a;
|
|
}
|