diff --git a/packages/engine/src/automaton.ts b/packages/engine/src/automaton.ts index 6020729..860a242 100644 --- a/packages/engine/src/automaton.ts +++ b/packages/engine/src/automaton.ts @@ -1071,11 +1071,19 @@ function selfCare(view: GameView, style: AutomatonStyle, tier: TierTraits): Comm } if (!tier.buffs) return null; // the apprentice's book ends at the stones // A curse on the clockwork gets scrubbed off. - const cursed = view.sustained.some( + // The engine wants the whole prescription — patient and ailment both — + // or the cast is refused and the fallback reads as an idle bot. + const myCurse = view.sustained.find( (e) => e.targetId === view.you && e.casterId !== view.you && AFFLICTIONS[e.cardId] != null, ); const cleanse = inHand(view, "remove-curse"); - if (cursed && cleanse) return { type: "cast", instanceId: cleanse.instanceId }; + if (myCurse && cleanse) { + return { + type: "cast", instanceId: cleanse.instanceId, + target: { kind: "player", playerId: view.you }, + params: { cardId: myCurse.cardId }, + }; + } // Three or more wizards: LIFESAVER takes elimination off the table. const lifesaver = inHand(view, "lifesaver"); if (lifesaver && view.players.length > 2) return { type: "cast", instanceId: lifesaver.instanceId }; diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index d06e543..177a755 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -521,7 +521,9 @@ function castSight( return !!cmd.aroundCornerInstanceId && bentLos(state, caster, caster.position, to); } -/** Sight for an edge-aimed cast (walls, doors), bend-aware like castSight. */ +/** Sight for an edge-aimed cast (walls, doors), bend-aware like castSight. + * VISIONSTONE is the bearer's sight here too — one wall or door, any one, + * falls away for the look, exactly as it does for square-aimed casts. */ function castSightEdge( state: GameState, caster: PlayerState, @@ -531,6 +533,14 @@ function castSightEdge( side: Side, ): boolean { if (losToEdge(board, caster.position, cell, side)) return true; + if (displays(caster, "visionstone")) { + for (const [key, edge] of Object.entries(board.edges)) { + if (edge === "open") continue; + const edges = { ...board.edges }; + delete edges[key]; + if (losToEdge({ ...board, edges }, caster.position, cell, side)) return true; + } + } if (!cmd.aroundCornerInstanceId) return false; for (const key of Object.keys(board.cells)) { const [mx, my] = key.split(",").map(Number) as [number, number]; @@ -1211,7 +1221,7 @@ const CARD_EFFECTS: Record } const key = edgeKey(cell, side); if ((view.edges[key] ?? "open") !== "open") return "there is already something in that wall line"; - if (!losToEdge(view, caster.position, cell, side)) return "no line of sight to the wall line"; + if (!castSightEdge(state, caster, cmd, view, cell, side)) return "no line of sight to the wall line"; state.edgeOverrides[key] = "wall"; state.createdEdges[key] = true; events.push({ type: "wallCreated", caster: caster.id, edge: { cell, side } }); @@ -1235,7 +1245,7 @@ const CARD_EFFECTS: Record const key = edgeKey(cell, side); const current = view.edges[key] ?? "open"; if (current === "open") return "there is no wall there"; - if (!losToEdge(view, caster.position, cell, side)) return "no line of sight to the wall"; + if (!castSightEdge(state, caster, cmd, view, cell, side)) return "no line of sight to the wall"; state.edgeOverrides[key] = "open"; delete state.doorStates[key]; delete state.createdEdges[key]; @@ -5122,7 +5132,7 @@ function doCast(prev: GameState, cmd: Extract): Comma if (effect.sameSquare && !touchesEdge(caster.position, cell, side)) { return err("you must stand beside the wall"); } - if (effect.requiresLos && !losToEdge(view, caster.position, cell, side)) { + if (effect.requiresLos && !castSightEdge(state, caster, cmd, view, cell, side)) { return err("no line of sight to the wall"); } const dmg =