Update effect resolver for indexed player/enemy targets
Added tests and verified resolveEffects handles {type, index}
descriptors for multi-player and multi-enemy state. getEntity and
setEntity helpers resolve either legacy string keys or typed index
objects, so all existing string-based call sites continue working.
This commit is contained in:
parent
86287a30c2
commit
e03b9b2dd7
2 changed files with 117 additions and 7 deletions
16
src/effects.js
vendored
16
src/effects.js
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue