A counter-spell aimed at the other wizard is explained, and named when it blocks
Blocked blows now say what blocked them (shield, counter-spell, dispel magic or protection from evil), the first counter-spell cast at the other wizard earns a one-time explanation that it protects its subject, and the spell's description says to cast it on yourself. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5de352db43
commit
dd6ff93606
@@ -289,7 +289,7 @@ describe('haste, time stop, delayed effect and permanency', () => {
|
|||||||
// The snap made in the stopped moment still counts toward a missile.
|
// The snap made in the stopped moment still counts toward a missile.
|
||||||
s = play(s, { right: 'D', casts: [{ hand: 'right', spellId: 'missile' }] });
|
s = play(s, { right: 'D', casts: [{ hand: 'right', spellId: 'missile' }] });
|
||||||
expect(s.wizards.B.hp).toBe(14);
|
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', () => {
|
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' }] });
|
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);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+24
-9
@@ -200,7 +200,9 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
|
|||||||
overlaps: [] as { name: string; spell: string; earlier: string }[],
|
overlaps: [] as { name: string; spell: string; earlier: string }[],
|
||||||
broken: [] as { name: string; hand: Hand }[],
|
broken: [] as { name: string; hand: Hand }[],
|
||||||
curedAndHurt: [] as { name: string; hurt: number; healed: number }[],
|
curedAndHurt: [] as { name: string; hurt: number; healed: number }[],
|
||||||
newMonsterAttacked: ''
|
newMonsterAttacked: '',
|
||||||
|
/** A counter-spell aimed at the other wizard, who is then protected by it. */
|
||||||
|
counterAimedAway: '' as string
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 1. Gestures, after last turn's enchantments have their say ----
|
// ---- 1. Gestures, after last turn's enchantments have their say ----
|
||||||
@@ -276,7 +278,8 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
|
|||||||
|
|
||||||
// ---- 2. Which completed spells are actually loosed ----
|
// ---- 2. Which completed spells are actually loosed ----
|
||||||
const casts: ActiveCast[] = [];
|
const casts: ActiveCast[] = [];
|
||||||
const shielded = new Set<TargetId>();
|
/** Who is shielded this turn, and by what, so a blocked blow can name it. */
|
||||||
|
const shielded = new Map<TargetId, string>();
|
||||||
for (const id of acting) {
|
for (const id of acting) {
|
||||||
const w = W[id];
|
const w = W[id];
|
||||||
const accepted: ActiveCast[] = [];
|
const accepted: ActiveCast[] = [];
|
||||||
@@ -364,7 +367,7 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
|
|||||||
if (dispels.length > 0) {
|
if (dispels.length > 0) {
|
||||||
log('Dispel magic sweeps the field: every other spell fails, all enchantments end, and every monster will fall after this turn.', 'spell');
|
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 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 id of IDS) clearEnchantments(W[id]);
|
||||||
for (const m of state.monsters) {
|
for (const m of state.monsters) {
|
||||||
clearMonsterEnchantments(m);
|
clearMonsterEnchantments(m);
|
||||||
@@ -441,11 +444,17 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
|
|||||||
|
|
||||||
// ---- 6. Who is shielded this turn ----
|
// ---- 6. Who is shielded this turn ----
|
||||||
for (const c of live()) {
|
for (const c of live()) {
|
||||||
if (['shield', 'counter_spell', 'dispel_magic', 'protection_from_evil'].includes(c.spell.id)) shielded.add(c.target);
|
if (c.spell.id === 'shield') shielded.set(c.target, 'shield');
|
||||||
|
if (c.spell.id === 'counter_spell') shielded.set(c.target, 'counter-spell');
|
||||||
|
if (c.spell.id === 'protection_from_evil') shielded.set(c.target, 'protection from evil');
|
||||||
|
if (c.spell.id === 'counter_spell' && isWizardId(c.target) && c.target !== c.caster) {
|
||||||
|
facts.counterAimedAway = `${W[c.caster].name}'s counter-spell on ${W[c.target].name}`;
|
||||||
}
|
}
|
||||||
for (const id of IDS) if (W[id].protectionTurns > 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 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 ----
|
// ---- 7. Summons, then storms and elementals ----
|
||||||
for (const c of live()) {
|
for (const c of live()) {
|
||||||
@@ -731,7 +740,7 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
|
|||||||
case 'missile':
|
case 'missile':
|
||||||
if (isShielded(t)) {
|
if (isShielded(t)) {
|
||||||
facts.shieldBlocked.add(t);
|
facts.shieldBlocked.add(t);
|
||||||
log(`${beingName(state, t)}'s shield turns the missile aside.`, 'fizzle');
|
log(`${guard(t)} turns the missile aside.`, 'fizzle');
|
||||||
} else hurt(t, 1, 'a missile');
|
} else hurt(t, 1, 'a missile');
|
||||||
break;
|
break;
|
||||||
case 'finger_of_death':
|
case 'finger_of_death':
|
||||||
@@ -826,7 +835,7 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
|
|||||||
}
|
}
|
||||||
if (isShielded(target)) {
|
if (isShielded(target)) {
|
||||||
facts.shieldBlocked.add(target);
|
facts.shieldBlocked.add(target);
|
||||||
log(`${m.name}'s blows glance off ${beingName(state, target)}'s shield.`, 'fizzle');
|
log(`${m.name}'s blows glance off ${guard(target)}.`, 'fizzle');
|
||||||
} else if (isWizardId(target) && W[target].invisibleTurns > 0 && !frozen(target)) {
|
} else if (isWizardId(target) && W[target].invisibleTurns > 0 && !frozen(target)) {
|
||||||
log(`${m.name} flails at the air where ${W[target].name} used to be.`, 'fizzle');
|
log(`${m.name} flails at the air where ${W[target].name} used to be.`, 'fizzle');
|
||||||
} else {
|
} else {
|
||||||
@@ -846,7 +855,7 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
|
|||||||
log(`${w.name} stabs at nothing.`, 'fizzle');
|
log(`${w.name} stabs at nothing.`, 'fizzle');
|
||||||
} else if (isShielded(target)) {
|
} else if (isShielded(target)) {
|
||||||
facts.shieldBlocked.add(target);
|
facts.shieldBlocked.add(target);
|
||||||
log(`${w.name}'s knife skids off ${beingName(state, target)}'s shield.`, 'fizzle');
|
log(`${w.name}'s knife skids off ${guard(target)}.`, 'fizzle');
|
||||||
} else {
|
} else {
|
||||||
hurt(target, 1, `${w.name}'s knife`);
|
hurt(target, 1, `${w.name}'s knife`);
|
||||||
}
|
}
|
||||||
@@ -960,6 +969,12 @@ export function resolveTurn(previous: GameState, inputs: Record<WizardId, TurnIn
|
|||||||
text: `A shield stops only creatures, missiles and stabs, so it did nothing against ${facts.throughShield[0].spell}. Counter-spell is the defence against other spells.`
|
text: `A shield stops only creatures, missiles and stabs, so it did nothing against ${facts.throughShield[0].spell}. Counter-spell is the defence against other spells.`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (facts.counterAimedAway) {
|
||||||
|
lessons.push({
|
||||||
|
key: 'counter-aim',
|
||||||
|
text: `A counter-spell protects its subject, not its caster: ${facts.counterAimedAway} cancelled spells aimed at them and shielded them from creatures, missiles and stabs. To stop what the other wizard casts at you, cast the counter-spell on yourself.`
|
||||||
|
});
|
||||||
|
}
|
||||||
if (facts.fingerThroughCounter) {
|
if (facts.fingerThroughCounter) {
|
||||||
lessons.push({ key: 'counter-finger', text: 'A counter-spell cannot stop finger of death. Only dispel magic can, or an anti-spell before the eight gestures are finished.' });
|
lessons.push({ key: 'counter-finger', text: 'A counter-spell cannot stop finger of death. Only dispel magic can, or an anti-spell before the eight gestures are finished.' });
|
||||||
} else if (facts.counterSmothered > 0) {
|
} else if (facts.counterSmothered > 0) {
|
||||||
|
|||||||
@@ -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('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('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('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('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('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.'),
|
spell('cure_light_wounds', 'Cure light wounds', 'protection', ['D-F-W'], 'self', 'Heals 1 damage.'),
|
||||||
|
|||||||
Reference in New Issue
Block a user