diff --git a/src/effects.js b/src/effects.js index bb1df67..be32590 100644 --- a/src/effects.js +++ b/src/effects.js @@ -36,11 +36,7 @@ function getEntity(state, descriptor) { function setEntity(state, descriptor, updated) { if (typeof descriptor === "string") { - const next = { ...state, [descriptor]: updated }; - // keep compat alias in sync - if (descriptor === "player") next.player = updated; - if (descriptor === "enemy") next.enemy = updated; - return next; + return { ...state, [descriptor]: updated }; } if (descriptor.type === "player") { const players = state.players.map((p, i) => @@ -77,9 +73,15 @@ function resolveSingleEffect(state, effect, source, target) { return applyStatus(state, target, "weak", effect.value, 3); case "strength": return applyStatus(state, source, "strength", effect.value, 8); - case "draw": - return drawCards(state, effect.value); + case "draw": { + const playerIdx = + typeof source === "object" && source.type === "player" + ? source.index + : 0; + return drawCards(state, playerIdx, effect.value); + } case "lose_hp": + // lose_hp damages the caster (source), not the target — e.g. Offering, Bloodletting return directDamage(state, source, effect.value); case "exhaust": // handled by playCard — card goes to exhaustPile instead of discardPile diff --git a/src/effects.test.js b/src/effects.test.js index 6612a9e..fffc9c9 100644 --- a/src/effects.test.js +++ b/src/effects.test.js @@ -148,3 +148,111 @@ describe("resolveEffects - draw", () => { expect(next.player.drawPile).toHaveLength(8); }); }); + +describe("resolveEffects - lose_hp", () => { + test("lose_hp damages the caster (source), not the target", () => { + const state = makeState(); + const enemyHpBefore = state.enemy.hp; + const playerHpBefore = state.player.hp; + const effects = [{ type: "lose_hp", value: 3 }]; + const next = resolveEffects(state, effects, "player", "enemy"); + expect(next.player.hp).toBe(playerHpBefore - 3); + expect(next.enemy.hp).toBe(enemyHpBefore); + }); +}); + +describe("resolveEffects - indexed descriptors", () => { + function makeMultiState(overrides = {}) { + const base = createCombatState( + ["ironclad", "ironclad"], + ["jaw_worm", "jaw_worm"], + ); + return { + ...base, + players: base.players.map((p, i) => + overrides.players?.[i] ? { ...p, ...overrides.players[i] } : p, + ), + enemies: base.enemies.map((e, i) => + overrides.enemies?.[i] ? { ...e, ...overrides.enemies[i] } : e, + ), + }; + } + + test("hit from player 0 reduces enemy 0 hp, enemy 1 unchanged", () => { + const state = makeMultiState(); + const effects = [{ type: "hit", value: 3 }]; + const next = resolveEffects( + state, + effects, + { type: "player", index: 0 }, + { type: "enemy", index: 0 }, + ); + expect(next.enemies[0].hp).toBeLessThan(state.enemies[0].hp); + expect(next.enemies[1].hp).toBe(state.enemies[1].hp); + }); + + test("hit from player 1 reduces enemy 1 hp, enemy 0 unchanged", () => { + const state = makeMultiState(); + const effects = [{ type: "hit", value: 2 }]; + const next = resolveEffects( + state, + effects, + { type: "player", index: 1 }, + { type: "enemy", index: 1 }, + ); + expect(next.enemies[1].hp).toBeLessThan(state.enemies[1].hp); + expect(next.enemies[0].hp).toBe(state.enemies[0].hp); + }); + + test("block applies to player 1 only", () => { + const state = makeMultiState(); + const effects = [{ type: "block", value: 4 }]; + const next = resolveEffects( + state, + effects, + { type: "player", index: 1 }, + { type: "enemy", index: 0 }, + ); + expect(next.players[1].block).toBe(4); + expect(next.players[0].block).toBe(0); + }); + + test("vulnerable applies to enemy 1 only", () => { + const state = makeMultiState(); + const effects = [{ type: "vulnerable", value: 2 }]; + const next = resolveEffects( + state, + effects, + { type: "player", index: 0 }, + { type: "enemy", index: 1 }, + ); + expect(next.enemies[1].vulnerable).toBe(2); + expect(next.enemies[0].vulnerable).toBe(0); + }); + + test("enemy hits player 0, player 1 hp unchanged", () => { + const state = makeMultiState(); + const effects = [{ type: "hit", value: 2 }]; + const next = resolveEffects( + state, + effects, + { type: "enemy", index: 0 }, + { type: "player", index: 0 }, + ); + expect(next.players[0].hp).toBeLessThan(state.players[0].hp); + expect(next.players[1].hp).toBe(state.players[1].hp); + }); + + test("draw effect draws for player 1, player 0 hand unchanged", () => { + const state = makeMultiState(); + const effects = [{ type: "draw", value: 3 }]; + const next = resolveEffects( + state, + effects, + { type: "player", index: 1 }, + { type: "enemy", index: 0 }, + ); + expect(next.players[1].hand).toHaveLength(3); + expect(next.players[0].hand).toHaveLength(0); + }); +});