diff --git a/src/lib/game/engine.test.ts b/src/lib/game/engine.test.ts index fd050cf..5be617f 100644 --- a/src/lib/game/engine.test.ts +++ b/src/lib/game/engine.test.ts @@ -289,7 +289,7 @@ describe('haste, time stop, delayed effect and permanency', () => { // The snap made in the stopped moment still counts toward a missile. s = play(s, { right: 'D', casts: [{ hand: 'right', spellId: 'missile' }] }); expect(s.wizards.B.hp).toBe(14); - expect(s.log.some((l) => l.text.includes('shield turns the missile aside'))).toBe(true); + expect(s.log.some((l) => l.text.includes('protection from evil turns the missile aside'))).toBe(true); }); it('delayed effect banks the next spell until it is released', () => { @@ -394,4 +394,15 @@ describe('lessons', () => { s = runSequence(s, 'A', 'SSDD', '----', { casts: [{ hand: 'left', spellId: 'fireball' }] }); expect(s.lastTurn?.lessons.some((l) => l.includes('reused gestures from the earlier Paralysis'))).toBe(true); }); + + it('explains a counter-spell aimed at the other wizard, who it then protects', () => { + let s = createGame({ A: 'Black', B: 'White' }, 1); + s = runSequence(s, 'A', 'SFW', '---', { casts: [{ hand: 'left', spellId: 'summon_goblin', monsterTarget: 'B' }] }); + expect(s.wizards.B.hp).toBe(14); + // The goblin lands twice more while the counter-spell is made, then the counter-spell shields White from it. + s = runSequence(s, 'A', 'WWS', '---', { casts: [{ hand: 'left', spellId: 'counter_spell', target: 'B' }] }); + expect(s.wizards.B.hp).toBe(12); + expect(s.log.some((l) => l.text === "Goblin's blows glance off White's counter-spell.")).toBe(true); + expect(s.lastTurn?.lessons.some((l) => l.includes('protects its subject, not its caster'))).toBe(true); + }); }); diff --git a/src/lib/game/resolve.ts b/src/lib/game/resolve.ts index 0b6aa8b..b9acd58 100644 --- a/src/lib/game/resolve.ts +++ b/src/lib/game/resolve.ts @@ -200,7 +200,9 @@ export function resolveTurn(previous: GameState, inputs: Record(); + /** Who is shielded this turn, and by what, so a blocked blow can name it. */ + const shielded = new Map(); for (const id of acting) { const w = W[id]; const accepted: ActiveCast[] = []; @@ -364,7 +367,7 @@ export function resolveTurn(previous: GameState, inputs: Record 0) { log('Dispel magic sweeps the field: every other spell fails, all enchantments end, and every monster will fall after this turn.', 'spell'); for (const c of casts) if (!c.nullified && c.spell.id !== 'dispel_magic') c.nullified = 'dispelled'; - for (const d of dispels) shielded.add(d.target); + for (const d of dispels) shielded.set(d.target, 'dispel magic'); for (const id of IDS) clearEnchantments(W[id]); for (const m of state.monsters) { clearMonsterEnchantments(m); @@ -441,11 +444,17 @@ export function resolveTurn(previous: GameState, inputs: Record 0) shielded.add(id); - for (const m of state.monsters) if (m.protectionTurns > 0) shielded.add(m.id); + for (const id of IDS) if (W[id].protectionTurns > 0 && !shielded.has(id)) shielded.set(id, 'protection from evil'); + for (const m of state.monsters) if (m.protectionTurns > 0 && !shielded.has(m.id)) shielded.set(m.id, 'protection from evil'); const isShielded = (id: TargetId) => shielded.has(id) && !frozenBeing(id); + const guard = (id: TargetId) => `${beingName(state, id)}'s ${shielded.get(id) ?? 'shield'}`; // ---- 7. Summons, then storms and elementals ---- for (const c of live()) { @@ -731,7 +740,7 @@ export function resolveTurn(previous: GameState, inputs: Record 0 && !frozen(target)) { log(`${m.name} flails at the air where ${W[target].name} used to be.`, 'fizzle'); } else { @@ -846,7 +855,7 @@ export function resolveTurn(previous: GameState, inputs: Record 0) { diff --git a/src/lib/game/spells.ts b/src/lib/game/spells.ts index 5dce555..59f5654 100644 --- a/src/lib/game/spells.ts +++ b/src/lib/game/spells.ts @@ -116,7 +116,7 @@ export const SPELLS: Spell[] = [ spell('shield', 'Shield', 'protection', ['P'], 'self', 'Blocks monsters, missiles and stabs against the subject this turn.'), spell('remove_enchantment', 'Remove enchantment', 'protection', ['P-D-W-P'], 'enemy', 'Ends every enchantment on the subject. Destroys a monster after it attacks.'), spell('magic_mirror', 'Magic mirror', 'protection', ['C-(w'], 'self', 'Spells cast at the subject this turn rebound on their caster. Undone by a counter-spell.'), - spell('counter_spell', 'Counter-spell', 'protection', ['W-P-P', 'W-W-S'], 'self', 'Other spells cast at the subject this turn fail, and the subject is shielded. Cannot stop finger of death or dispel magic.'), + spell('counter_spell', 'Counter-spell', 'protection', ['W-P-P', 'W-W-S'], 'self', 'Every other spell cast at the subject this turn fails, and the subject is shielded. Cast it on yourself to stop what the other wizard casts at you. Cannot stop finger of death or dispel magic.'), spell('dispel_magic', 'Dispel magic', 'protection', ['C-D-P-W'], 'self', 'Every other spell this turn fails, all enchantments end, all monsters die after attacking. Shields the subject.'), spell('raise_dead', 'Raise dead', 'protection', ['D-W-W-F-W-C'], 'self', 'Heals 5 damage and removes all enchantments from the subject.'), spell('cure_light_wounds', 'Cure light wounds', 'protection', ['D-F-W'], 'self', 'Heals 1 damage.'),