Fear repels every willing step (rules rev 32)

Room QD3S: Kestrel cast Fear and a Democratic Monster warp-stepped from
eight squares away to ONE — moveCreature never knew the card existed.
'No player or monster will move to within 3 spaces of you' now guards
every willing move: walking wizards (as before), commanded monsters
(walks, wall-passes, and warp mouths alike), dimensional warp-steps,
and self-teleports. Forced movement — knockback, waves, Mental Force,
Teleport Opponent — is not willing, and still throws you anywhere.
The shared fearRepels helper replaces the inline wizard-only check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-19 21:23:47 -04:00
co-authored by Claude Fable 5
parent 172fbc6ff8
commit 0e9d482495
4 changed files with 66 additions and 12 deletions
+33 -9
View File
@@ -1234,6 +1234,10 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
if (wallIgnoringDistance(view, caster.position, to) > 4) {
return "teleport reaches at most four spaces";
}
// A teleport is a willing move: FEAR's bubble refuses it (rev 32).
if ((state.config.deckRev ?? 1) >= 32 && fearRepels(state, caster.id, caster.position, to)) {
return "an unnatural dread keeps you away";
}
const from = caster.position;
caster.position = to;
state.turn.movementUsed = state.turn.movementAllowance; // movement ends
@@ -2867,6 +2871,12 @@ function doMoveCreature(prev: GameState, creatureId: string, direction: Side): C
}
creature.position = target.to;
}
// FEAR holds monsters off too (rules rev 32): "no player or monster".
if ((state.config.deckRev ?? 1) >= 32 && fearRepels(state, null, from, creature.position)) {
creature.position = from;
if (creature.wallPassUsed > 0) creature.wallPassUsed--;
return err("an unnatural dread stops the beast");
}
creature.movementUsed++;
events.push({ type: "creatureMoved", creatureId: creature.id, from, to: creature.position, direction, by: active.id });
@@ -3706,6 +3716,23 @@ function takeFromHand(p: PlayerState, instanceId: string): CardInstance | null {
// --- Movement ---------------------------------------------------------------
/**
* FEAR: "no player or monster will move to within 3 spaces of you." True
* for every WILLING move walking, warp steps, self-teleports, commanded
* monsters (rules rev 32 widened it beyond walking wizards; forced movement
* such as knockback or a wave is not willing and passes).
*/
function fearRepels(state: GameState, moverId: PlayerId | null, from: Cell, to: Cell): boolean {
for (const other of state.players) {
if (!other.alive || other.id === moverId) continue;
if (sustainedOn(state, other.id, "fear").length === 0) continue;
const d = Math.abs(other.position.x - to.x) + Math.abs(other.position.y - to.y);
const dBefore = Math.abs(other.position.x - from.x) + Math.abs(other.position.y - from.y);
if (d <= 3 && d < dBefore) return true;
}
return false;
}
/** A blind wizard's wasted lurch into a wall still costs a movement point. */
function blindBump(state: GameState, events: GameEvent[], p: PlayerState, direction: Side): CommandResult {
state.turn.movementUsed++;
@@ -3860,15 +3887,9 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
}
}
// FEAR: no one moves within 3 spaces of the fearsome one.
for (const other of state.players) {
if (other.id === p.id || !other.alive) continue;
if (sustainedOn(state, other.id, "fear").length === 0) continue;
const d = Math.abs(other.position.x - p.position.x) + Math.abs(other.position.y - p.position.y);
const dBefore = Math.abs(other.position.x - from.x) + Math.abs(other.position.y - from.y);
if (d <= 3 && d < dBefore) {
p.position = from;
return err("an unnatural dread keeps you away");
}
if (fearRepels(state, p.id, from, p.position)) {
p.position = from;
return err("an unnatural dread keeps you away");
}
// CREATE PIT: stepping onto a pit is a jump attempt — roll D4; on a 1 you
@@ -4029,6 +4050,9 @@ function doWarpStep(prev: GameState): CommandResult {
const dest = cellKey(pair.a) === here ? pair.b : pair.a;
if (state.squareContents[cellKey(dest)]?.kind === "stone") return err("the far side is solid stone");
const from = p.position;
if ((state.config.deckRev ?? 1) >= 32 && fearRepels(state, p.id, from, dest)) {
return err("an unnatural dread keeps you away");
}
p.position = { ...dest };
state.turn.movementUsed++;
return { ok: true, state, events: [{ type: "warpStepped", player: p.id, from, to: p.position }] };