From a1f242d54eff36b90c9ce164af2306e0708c2f85 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Mon, 23 Feb 2026 17:34:49 -0500 Subject: [PATCH] Add state module with combat init, draw, play, end turn --- src/effects.js | 3 ++ src/enemies.js | 5 +++ src/state.js | 106 ++++++++++++++++++++++++++++++++++++++++++++++ src/state.test.js | 74 ++++++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+) create mode 100644 src/effects.js create mode 100644 src/enemies.js create mode 100644 src/state.js create mode 100644 src/state.test.js diff --git a/src/effects.js b/src/effects.js new file mode 100644 index 0000000..6e984f7 --- /dev/null +++ b/src/effects.js @@ -0,0 +1,3 @@ +export function resolveEffects(state) { + return state; +} diff --git a/src/enemies.js b/src/enemies.js new file mode 100644 index 0000000..7ac88ec --- /dev/null +++ b/src/enemies.js @@ -0,0 +1,5 @@ +import enemyDb from "../data/enemies.json"; + +export function getEnemy(id) { + return enemyDb[id]; +} diff --git a/src/state.js b/src/state.js new file mode 100644 index 0000000..aad9f0f --- /dev/null +++ b/src/state.js @@ -0,0 +1,106 @@ +import { getCard, getStarterDeck } from "./cards.js"; +import { resolveEffects } from "./effects.js"; +import { getEnemy } from "./enemies.js"; + +export function createCombatState(character, enemyId) { + const enemy = getEnemy(enemyId); + return { + player: { + hp: 11, + maxHp: 11, + energy: 3, + maxEnergy: 3, + block: 0, + strength: 0, + vulnerable: 0, + weak: 0, + drawPile: shuffle([...getStarterDeck(character)]), + hand: [], + discardPile: [], + exhaustPile: [], + powers: [], + }, + enemy: { + id: enemy.id, + 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, + }, + combat: { + turn: 1, + phase: "player_turn", + dieResult: null, + selectedCard: null, + log: [], + }, + }; +} + +export function drawCards(state, count) { + let drawPile = [...state.player.drawPile]; + let discardPile = [...state.player.discardPile]; + const hand = [...state.player.hand]; + + for (let i = 0; i < count; i++) { + if (drawPile.length === 0) { + drawPile = shuffle(discardPile); + discardPile = []; + } + if (drawPile.length > 0) { + hand.push(drawPile.pop()); + } + } + + return { + ...state, + player: { ...state.player, drawPile, hand, discardPile }, + }; +} + +export function playCard(state, handIndex) { + const cardId = state.player.hand[handIndex]; + const card = getCard(cardId); + if (state.player.energy < card.cost) return null; + + const hand = [...state.player.hand]; + hand.splice(handIndex, 1); + const discardPile = [...state.player.discardPile, cardId]; + const energy = state.player.energy - card.cost; + + let next = { + ...state, + player: { ...state.player, hand, discardPile, energy }, + }; + + next = resolveEffects(next, card.effects, "player", "enemy"); + return next; +} + +export function endTurn(state) { + return { + ...state, + player: { + ...state.player, + hand: [], + discardPile: [...state.player.discardPile, ...state.player.hand], + }, + combat: { ...state.combat, phase: "enemy_turn" }, + }; +} + +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; +} diff --git a/src/state.test.js b/src/state.test.js new file mode 100644 index 0000000..5708f20 --- /dev/null +++ b/src/state.test.js @@ -0,0 +1,74 @@ +import { describe, expect, test } from "bun:test"; +import { createCombatState, drawCards, endTurn, playCard } from "./state.js"; + +describe("createCombatState", () => { + test("creates initial state with shuffled deck and correct values", () => { + const state = createCombatState("ironclad", "jaw_worm"); + expect(state.player.hp).toBe(11); + expect(state.player.maxHp).toBe(11); + expect(state.player.energy).toBe(3); + expect(state.player.block).toBe(0); + expect(state.player.strength).toBe(0); + expect(state.player.hand).toHaveLength(0); + expect(state.player.drawPile).toHaveLength(10); + expect(state.enemy.name).toBe("Jaw Worm"); + expect(state.enemy.hp).toBeGreaterThan(0); + expect(state.combat.turn).toBe(1); + expect(state.combat.phase).toBe("player_turn"); + }); +}); + +describe("drawCards", () => { + test("moves cards from draw pile to hand", () => { + const state = createCombatState("ironclad", "jaw_worm"); + const next = drawCards(state, 5); + expect(next.player.hand).toHaveLength(5); + expect(next.player.drawPile).toHaveLength(5); + }); + + test("shuffles discard into draw when draw pile runs out", () => { + let state = createCombatState("ironclad", "jaw_worm"); + state = { + ...state, + player: { + ...state.player, + drawPile: state.player.drawPile.slice(0, 2), + discardPile: state.player.drawPile.slice(2), + }, + }; + const next = drawCards(state, 5); + expect(next.player.hand).toHaveLength(5); + expect(next.player.discardPile).toHaveLength(0); + }); +}); + +describe("playCard", () => { + test("deducts energy and moves card to discard", () => { + let state = createCombatState("ironclad", "jaw_worm"); + state = drawCards(state, 5); + const cardIndex = state.player.hand.indexOf("strike_r"); + const next = playCard(state, cardIndex); + expect(next.player.energy).toBe(2); + expect(next.player.hand).toHaveLength(4); + }); + + test("returns null if not enough energy", () => { + let state = createCombatState("ironclad", "jaw_worm"); + state = drawCards(state, 5); + state = { ...state, player: { ...state.player, energy: 0 } }; + const cardIndex = state.player.hand.indexOf("strike_r"); + const result = playCard(state, cardIndex); + expect(result).toBeNull(); + }); +}); + +describe("endTurn", () => { + test("discards hand and switches to enemy phase", () => { + let state = createCombatState("ironclad", "jaw_worm"); + state = drawCards(state, 5); + const next = endTurn(state); + expect(next.player.hand).toHaveLength(0); + expect(next.player.discardPile.length).toBeGreaterThan(0); + expect(next.combat.phase).toBe("enemy_turn"); + }); +});