Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81acbd2172 | ||
|
|
dd6ff93606 |
No files matched your search
+4
-35
@@ -16,6 +16,7 @@ import {
|
|||||||
type TurnGestures
|
type TurnGestures
|
||||||
} from './gestures';
|
} from './gestures';
|
||||||
import { allowedGestures, forcedGesture } from './resolve';
|
import { allowedGestures, forcedGesture } from './resolve';
|
||||||
|
import { likelyTarget } from './targets';
|
||||||
import { GESTURES, HANDS, HOSTILE_SPELLS, MAGIC_GESTURES, SPELLS, SPELL_BY_ID, isElemental, type Gesture, type Hand, type Spell, type SpellId } from './spells';
|
import { GESTURES, HANDS, HOSTILE_SPELLS, MAGIC_GESTURES, SPELLS, SPELL_BY_ID, isElemental, type Gesture, type Hand, type Spell, type SpellId } from './spells';
|
||||||
import {
|
import {
|
||||||
MAX_HP,
|
MAX_HP,
|
||||||
@@ -330,41 +331,9 @@ function foeDangerousHand(s: Situation): Hand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function targetFor(s: Situation, spell: Spell, choice: CastChoice): void {
|
function targetFor(s: Situation, spell: Spell, choice: CastChoice): void {
|
||||||
const meW = s.state.wizards[s.me];
|
choice.target = likelyTarget(s.state, s.me, spell);
|
||||||
const foeMonsters = s.state.monsters.filter((m) => m.owner === s.foe);
|
if (spell.id === 'summon_elemental') choice.elemental = s.state.wizards[s.me].resistCold ? 'ice' : 'fire';
|
||||||
const strongestFoeMonster = [...foeMonsters].sort((a, b) => b.attack - a.attack)[0];
|
if (spell.id === 'paralysis' || spell.id === 'charm_person') choice.chosenHand = foeDangerousHand(s);
|
||||||
const iceEl = s.state.monsters.find((m) => m.kind === 'ice_elemental');
|
|
||||||
const fireEl = s.state.monsters.find((m) => m.kind === 'fire_elemental');
|
|
||||||
switch (spell.id) {
|
|
||||||
case 'fireball':
|
|
||||||
choice.target = iceEl ? iceEl.id : s.foe;
|
|
||||||
break;
|
|
||||||
case 'resist_cold':
|
|
||||||
choice.target = iceEl ? iceEl.id : s.me;
|
|
||||||
break;
|
|
||||||
case 'resist_heat':
|
|
||||||
choice.target = fireEl ? fireEl.id : s.me;
|
|
||||||
break;
|
|
||||||
case 'charm_monster':
|
|
||||||
choice.target = strongestFoeMonster?.id ?? s.foe;
|
|
||||||
break;
|
|
||||||
case 'remove_enchantment': {
|
|
||||||
const foeW = s.state.wizards[s.foe];
|
|
||||||
choice.target = isEnchanted(foeW) || !strongestFoeMonster ? s.foe : strongestFoeMonster.id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'summon_elemental':
|
|
||||||
choice.target = s.me;
|
|
||||||
choice.elemental = meW.resistCold ? 'ice' : 'fire';
|
|
||||||
break;
|
|
||||||
case 'paralysis':
|
|
||||||
case 'charm_person':
|
|
||||||
choice.target = s.foe;
|
|
||||||
choice.chosenHand = foeDangerousHand(s);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
choice.target = spell.usualTarget === 'self' ? s.me : s.foe;
|
|
||||||
}
|
|
||||||
if (spell.category === 'summons') choice.monsterTarget = s.foe;
|
if (spell.category === 'summons') choice.monsterTarget = s.foe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from './gestures';
|
} from './gestures';
|
||||||
import { glyph } from './glyphs';
|
import { glyph } from './glyphs';
|
||||||
import { allowedGestures, forcedGesture, resolveTurn } from './resolve';
|
import { allowedGestures, forcedGesture, resolveTurn } from './resolve';
|
||||||
|
import { likelyTarget } from './targets';
|
||||||
import {
|
import {
|
||||||
GESTURES,
|
GESTURES,
|
||||||
HANDS,
|
HANDS,
|
||||||
@@ -72,11 +73,6 @@ export interface PlannedCast {
|
|||||||
turnCount: number;
|
turnCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultTarget(spell: Spell | undefined): TargetId | '' {
|
|
||||||
if (!spell) return '';
|
|
||||||
return spell.usualTarget === 'self' ? YOU : FOE;
|
|
||||||
}
|
|
||||||
|
|
||||||
function blankDraft(): HandDraft {
|
function blankDraft(): HandDraft {
|
||||||
return { gesture: null, spellId: '', target: '', elemental: 'fire', chosenHand: 'left' };
|
return { gesture: null, spellId: '', target: '', elemental: 'fire', chosenHand: 'left' };
|
||||||
}
|
}
|
||||||
@@ -541,12 +537,22 @@ export class Duel {
|
|||||||
);
|
);
|
||||||
bankedSpell = $derived(this.you.banked ? SPELL_BY_ID[this.you.banked.spellId] : undefined);
|
bankedSpell = $derived(this.you.banked ? SPELL_BY_ID[this.you.banked.spellId] : undefined);
|
||||||
|
|
||||||
|
/** Where a spell goes unless the player says otherwise. */
|
||||||
|
defaultTarget(spell: Spell | undefined): TargetId | '' {
|
||||||
|
return spell ? likelyTarget(this.state, YOU, spell) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Every possible subject, the likeliest first, creatures marked by whose they are. */
|
||||||
targetsFor(spell: Spell | undefined): { id: TargetId; label: string }[] {
|
targetsFor(spell: Spell | undefined): { id: TargetId; label: string }[] {
|
||||||
const list: { id: TargetId; label: string }[] = [
|
const list: { id: TargetId; label: string }[] = [
|
||||||
{ id: YOU, label: 'yourself' },
|
{ id: YOU, label: 'yourself' },
|
||||||
{ id: FOE, label: this.foe.name }
|
{ id: FOE, label: this.foe.name }
|
||||||
];
|
];
|
||||||
for (const m of this.state.monsters) list.push({ id: m.id, label: m.name });
|
for (const m of this.state.monsters) {
|
||||||
|
list.push({ id: m.id, label: `${m.name} (${m.owner === YOU ? 'yours' : `${this.foe.name}'s`})` });
|
||||||
|
}
|
||||||
|
const likely = this.defaultTarget(spell);
|
||||||
|
list.sort((a, b) => Number(b.id === likely) - Number(a.id === likely));
|
||||||
if (spell && spell.id !== 'fire_storm' && spell.id !== 'ice_storm') list.push({ id: NOWHERE, label: 'nowhere (let it fade)' });
|
if (spell && spell.id !== 'fire_storm' && spell.id !== 'ice_storm') list.push({ id: NOWHERE, label: 'nowhere (let it fade)' });
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -563,13 +569,13 @@ export class Duel {
|
|||||||
const options = this.completions[set][hand];
|
const options = this.completions[set][hand];
|
||||||
if (!options.some((c) => c.spell.id === draft.spellId)) {
|
if (!options.some((c) => c.spell.id === draft.spellId)) {
|
||||||
draft.spellId = options[0]?.spell.id ?? '';
|
draft.spellId = options[0]?.spell.id ?? '';
|
||||||
draft.target = defaultTarget(options[0]?.spell);
|
draft.target = this.defaultTarget(options[0]?.spell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSpell(set: SetName, hand: Hand, id: SpellId | ''): void {
|
setSpell(set: SetName, hand: Hand, id: SpellId | ''): void {
|
||||||
this.sets[set][hand].spellId = id;
|
this.sets[set][hand].spellId = id;
|
||||||
this.sets[set][hand].target = defaultTarget(id ? SPELL_BY_ID[id] : undefined);
|
this.sets[set][hand].target = this.defaultTarget(id ? SPELL_BY_ID[id] : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
private castFor(p: PlannedCast): CastChoice {
|
private castFor(p: PlannedCast): CastChoice {
|
||||||
@@ -578,7 +584,7 @@ export class Duel {
|
|||||||
hand: p.hand,
|
hand: p.hand,
|
||||||
spellId: spell.id,
|
spellId: spell.id,
|
||||||
seqIndex: p.completion.seqIndex,
|
seqIndex: p.completion.seqIndex,
|
||||||
target: p.draft.target || defaultTarget(spell)
|
target: p.draft.target || this.defaultTarget(spell)
|
||||||
};
|
};
|
||||||
if (spell.id === 'summon_elemental') cast.elemental = p.draft.elemental;
|
if (spell.id === 'summon_elemental') cast.elemental = p.draft.elemental;
|
||||||
if (spell.id === 'paralysis' || spell.id === 'charm_person') cast.chosenHand = p.draft.chosenHand;
|
if (spell.id === 'paralysis' || spell.id === 'charm_person') cast.chosenHand = p.draft.chosenHand;
|
||||||
|
|||||||
@@ -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 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.add(m.id);
|
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.'),
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { SPELL_BY_ID } from './spells';
|
||||||
|
import { createGame } from './state';
|
||||||
|
import { likelyTarget } from './targets';
|
||||||
|
import { runSequence } from './test-helpers';
|
||||||
|
|
||||||
|
describe('the likeliest target', () => {
|
||||||
|
it('sends a fireball and resist cold at an ice elemental when one is out', () => {
|
||||||
|
let s = createGame({ A: 'Black', B: 'White' }, 1);
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.fireball)).toBe('B');
|
||||||
|
s = runSequence(s, 'B', 'CSWWS', 'C----', { casts: [{ hand: 'left', spellId: 'summon_elemental', elemental: 'ice' }] });
|
||||||
|
const elemental = s.monsters.find((m) => m.kind === 'ice_elemental')!;
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.fireball)).toBe(elemental.id);
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.resist_cold)).toBe(elemental.id);
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.resist_heat)).toBe('A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aims remove enchantment at the wizard only when they carry an enchantment', () => {
|
||||||
|
let s = createGame({ A: 'Black', B: 'White' }, 1);
|
||||||
|
s = runSequence(s, 'B', 'SFW', '---', { casts: [{ hand: 'left', spellId: 'summon_goblin' }] });
|
||||||
|
const goblin = s.monsters[0];
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.remove_enchantment)).toBe(goblin.id);
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.charm_monster)).toBe(goblin.id);
|
||||||
|
s = runSequence(s, 'B', 'WWFP', '----', { casts: [{ hand: 'left', spellId: 'resist_heat' }] });
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.remove_enchantment)).toBe('B');
|
||||||
|
expect(likelyTarget(s, 'A', SPELL_BY_ID.counter_spell)).toBe('A');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Where a spell most likely goes, given what is on the field. Both the bot and
|
||||||
|
// the target dropdown start from this; the player can always aim elsewhere.
|
||||||
|
|
||||||
|
import { isElemental, type Spell } from './spells';
|
||||||
|
import { enemyOf, isEnchanted, type GameState, type TargetId, type WizardId } from './state';
|
||||||
|
|
||||||
|
export function likelyTarget(state: GameState, me: WizardId, spell: Spell): TargetId {
|
||||||
|
const foe = enemyOf(me);
|
||||||
|
const monsters = state.monsters;
|
||||||
|
const foeMonsters = monsters.filter((m) => m.owner === foe && !isElemental(m.kind));
|
||||||
|
const strongestFoeMonster = [...foeMonsters].sort((a, b) => b.attack - a.attack)[0];
|
||||||
|
const iceElemental = monsters.find((m) => m.kind === 'ice_elemental');
|
||||||
|
const fireElemental = monsters.find((m) => m.kind === 'fire_elemental');
|
||||||
|
switch (spell.id) {
|
||||||
|
case 'fireball':
|
||||||
|
return iceElemental?.id ?? foe;
|
||||||
|
case 'resist_cold':
|
||||||
|
return iceElemental?.id ?? me;
|
||||||
|
case 'resist_heat':
|
||||||
|
return fireElemental?.id ?? me;
|
||||||
|
case 'charm_monster':
|
||||||
|
return strongestFoeMonster?.id ?? foe;
|
||||||
|
case 'remove_enchantment':
|
||||||
|
return isEnchanted(state.wizards[foe]) || !strongestFoeMonster ? foe : strongestFoeMonster.id;
|
||||||
|
default:
|
||||||
|
return spell.usualTarget === 'self' ? me : foe;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user