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) {
|
function setEntity(state, descriptor, updated) {
|
||||||
if (typeof descriptor === "string") {
|
if (typeof descriptor === "string") {
|
||||||
const next = { ...state, [descriptor]: updated };
|
return { ...state, [descriptor]: updated };
|
||||||
// keep compat alias in sync
|
|
||||||
if (descriptor === "player") next.player = updated;
|
|
||||||
if (descriptor === "enemy") next.enemy = updated;
|
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
if (descriptor.type === "player") {
|
if (descriptor.type === "player") {
|
||||||
const players = state.players.map((p, i) =>
|
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);
|
return applyStatus(state, target, "weak", effect.value, 3);
|
||||||
case "strength":
|
case "strength":
|
||||||
return applyStatus(state, source, "strength", effect.value, 8);
|
return applyStatus(state, source, "strength", effect.value, 8);
|
||||||
case "draw":
|
case "draw": {
|
||||||
return drawCards(state, effect.value);
|
const playerIdx =
|
||||||
|
typeof source === "object" && source.type === "player"
|
||||||
|
? source.index
|
||||||
|
: 0;
|
||||||
|
return drawCards(state, playerIdx, effect.value);
|
||||||
|
}
|
||||||
case "lose_hp":
|
case "lose_hp":
|
||||||
|
// lose_hp damages the caster (source), not the target — e.g. Offering, Bloodletting
|
||||||
return directDamage(state, source, effect.value);
|
return directDamage(state, source, effect.value);
|
||||||
case "exhaust":
|
case "exhaust":
|
||||||
// handled by playCard — card goes to exhaustPile instead of discardPile
|
// handled by playCard — card goes to exhaustPile instead of discardPile
|
||||||
|
|
|
||||||
|
|
@ -148,3 +148,111 @@ describe("resolveEffects - draw", () => {
|
||||||
expect(next.player.drawPile).toHaveLength(8);
|
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