The Alter Ego casts from its own square; the first-stride cue leaves the maze

Randy raised his double, selected it, and could cast nothing through
it: the card's whole point — "it may use any of the spells in your
hand, and you need not be in its L.O.S." — had never been wired. A
cast may now name the double, and the double's square becomes the
spell's origin: sight, reach, the launch point the reel films, and
the direction a knockback or rout pushes. The hand, the attack, and
every answer to it stay the caster's. Spells that move the caster's
own body — a goat's charge, a swap — do not travel through it. On the
board, the double's sight lights the eligible squares; selecting the
double and then a card aims from where it stands. A test hides the
caster and fires through the double.

The first-stride caption sat on the maze and hid the walls beside the
wizard; it now stands above the board with a color chip for "you."

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-15 23:38:58 -04:00
co-authored by Claude Fable 5.1
parent 5f58a0955a
commit 10d5c5815d
5 changed files with 158 additions and 78 deletions
+13 -10
View File
@@ -220,20 +220,23 @@ export function fearCells(view: GameView): Set<string> {
* reflects what this player knows (illusion walls they believe in block it).
* The basis for the client's "dim the ineligible squares" targeting aid.
*/
export function sightedCellsFor(view: GameView): Set<string> {
export function sightedCellsFor(view: GameView, from?: Cell): Set<string> {
const out = new Set<string>();
const me = view.players.find((p) => p.id === view.you);
if (!me) return out;
// Sight is taken from the viewer's square unless a cast leaves from
// elsewhere — an ALTER EGO's — in which case it is the double's.
const eye = from ?? me.position;
const { board, blockers } = sightBasis(view);
// In a dust cloud a wizard sees only their own square (rev 19).
if (dustAtEnd(view, me.position, me.position, true)) {
out.add(`${me.position.x},${me.position.y}`);
if (dustAtEnd(view, eye, eye, true)) {
out.add(`${eye.x},${eye.y}`);
return out;
}
for (const key of Object.keys(board.cells)) {
const [x, y] = key.split(",").map(Number) as [number, number];
if (dustAtEnd(view, me.position, { x, y })) continue;
if (sightBetween(board, me.position, { x, y }, blockers)) out.add(key);
if (dustAtEnd(view, eye, { x, y })) continue;
if (sightBetween(board, eye, { x, y }, blockers)) out.add(key);
}
// VISIONSTONE lets its bearer see through exactly one wall or door —
// any one — so a square is also sighted if removing a single edge
@@ -248,7 +251,7 @@ export function sightedCellsFor(view: GameView): Set<string> {
const opened = { ...board, edges };
for (let i = unseen.length - 1; i >= 0; i--) {
const [x, y] = unseen[i]!.split(",").map(Number) as [number, number];
if (sightBetween(opened, me.position, { x, y }, blockers)) {
if (sightBetween(opened, eye, { x, y }, blockers)) {
out.add(unseen[i]!);
unseen.splice(i, 1);
}
@@ -263,8 +266,8 @@ export function sightedCellsFor(view: GameView): Set<string> {
* plus every square visible from one of those — the caster looks to a middle
* cell and the spell turns there, mirroring the engine's bentLos.
*/
export function bentSightedCellsFor(view: GameView): Set<string> {
const out = sightedCellsFor(view);
export function bentSightedCellsFor(view: GameView, from?: Cell): Set<string> {
const out = sightedCellsFor(view, from);
const me = view.players.find((p) => p.id === view.you);
if (!me) return out;
const mids = [...out].map((k) => {
@@ -431,12 +434,12 @@ const SUMMON_CARD_IDS = new Set([
* aid — mirroring the engine's own validation from the viewer's knowledge.
* Null = this card's eligibility is not modeled; light everything.
*/
export function eligibleCellsFor(view: GameView, cardId: string, bentCorner = false): Set<string> | null {
export function eligibleCellsFor(view: GameView, cardId: string, bentCorner = false, from?: Cell): Set<string> | null {
const me = view.players.find((p) => p.id === view.you);
if (!me) return null;
const cells = Object.keys(view.board.cells);
const key = (x: number, y: number) => `${x},${y}`;
const sighted = bentCorner ? bentSightedCellsFor(view) : sightedCellsFor(view);
const sighted = bentCorner ? bentSightedCellsFor(view, from) : sightedCellsFor(view, from);
if (CREATION_CARD_IDS.has(cardId)) {
// emptySquareTarget: on the board, unoccupied by content, home, wizard,