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:
co-authored by
Claude Fable 5.1
parent
5f58a0955a
commit
10d5c5815d
+50
-14
@@ -590,8 +590,15 @@
|
||||
|
||||
/** Attachment plumbing for a cast command: the chosen mods and the
|
||||
* attached NUMBER ride along (an explicit numberInstanceIds wins). */
|
||||
/** Your ALTER EGO, when it is the selected token: casts leave from its square. */
|
||||
const castingDouble = $derived(
|
||||
selectedCreature && view
|
||||
? view.creatures.find((c) => c.id === selectedCreature && c.kind === "alter-ego" && c.controllerId === view!.you) ?? null
|
||||
: null,
|
||||
);
|
||||
function withMods<T extends Parameters<typeof net.command>[0] & { type: "cast" }>(cmd: T): T {
|
||||
applyMods(cmd);
|
||||
if (castingDouble && cmd.target) cmd.via = castingDouble.id;
|
||||
if (attachedNumber && !cmd.numberInstanceIds) {
|
||||
cmd.numberInstanceIds = [attachedNumber.instanceId];
|
||||
}
|
||||
@@ -1150,7 +1157,7 @@
|
||||
|
||||
function clickPlayer(playerId: string) {
|
||||
if (!view || !yourMoment) return;
|
||||
if (selectedCreature) {
|
||||
if (selectedCreature && !(castingDouble && selectedCard)) {
|
||||
dispatch({ type: "creatureAttack", creatureId: selectedCreature, targetId: playerId });
|
||||
selectedCreature = null;
|
||||
return;
|
||||
@@ -1540,12 +1547,9 @@
|
||||
.filter((t) => t.kind !== "blocked")
|
||||
.map((t) => cellKey(t.to));
|
||||
});
|
||||
/** The first stride of a first game gets one line on the board itself. */
|
||||
const stepCaption = $derived(
|
||||
stepLight && view && me && view.turn.round === 1 && view.turn.movementUsed === 0
|
||||
? { cell: me.position, text: "This is you — tap a lit square to walk" }
|
||||
: null,
|
||||
);
|
||||
/** The first stride of a first game gets one line above the board —
|
||||
* off the maze, so the walls around the wizard stay in plain sight. */
|
||||
const stepCaption = $derived(stepLight && view && me && view.turn.round === 1 && view.turn.movementUsed === 0);
|
||||
const litCells = $derived.by(() => {
|
||||
if (view && me && !selectedDef && stepLight) return new Set([cellKey(me.position), ...stepCells]);
|
||||
if (!view || !selectedDef || !yourMoment) return null;
|
||||
@@ -1553,10 +1557,10 @@
|
||||
if (!me) return null;
|
||||
const bent = attachedMods.some((m) => m.cardId === "around-the-corner");
|
||||
if (CELL_CARDS.has(selectedCard!.cardId)) {
|
||||
return eligibleCellsFor(view, selectedCard!.cardId, bent);
|
||||
return eligibleCellsFor(view, selectedCard!.cardId, bent, castingDouble?.position);
|
||||
}
|
||||
if (selectedDef.adjacent === true) {
|
||||
const { x, y } = me.position;
|
||||
const { x, y } = castingDouble?.position ?? me.position;
|
||||
return new Set(
|
||||
[[x, y], [x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]
|
||||
.map(([cx, cy]) => `${cx},${cy}`)
|
||||
@@ -1564,7 +1568,7 @@
|
||||
);
|
||||
}
|
||||
if (selectedDef.los !== true) return null;
|
||||
return bent ? bentSightedCellsFor(view) : sightedCellsFor(view);
|
||||
return bent ? bentSightedCellsFor(view, castingDouble?.position) : sightedCellsFor(view, castingDouble?.position);
|
||||
});
|
||||
|
||||
/** Creatures awaiting your orders this turn (yours + any democratic monster). */
|
||||
@@ -2479,8 +2483,12 @@
|
||||
{#if selectedCreature && !selectedDef}
|
||||
{@const sc = view.creatures.find((c) => c.id === selectedCreature)}
|
||||
{#if sc}
|
||||
<span>Commanding <strong>{cardDef(sc.kind).name}</strong> — {creatureStats(sc)}.
|
||||
Tap a square beside it to march, a target in its square to attack.</span>
|
||||
{#if sc.kind === "alter-ego"}
|
||||
<span>Your <strong>double</strong> stands ready — select a card and it casts from the double's square, by the double's sight.</span>
|
||||
{:else}
|
||||
<span>Commanding <strong>{cardDef(sc.kind).name}</strong> — {creatureStats(sc)}.
|
||||
Tap a square beside it to march, a target in its square to attack.</span>
|
||||
{/if}
|
||||
{#if dmWarnCell}
|
||||
<span class="hint-alert">⚠ it will claw YOU the moment it enters your square — tap again if you mean it</span>
|
||||
{/if}
|
||||
@@ -2673,6 +2681,12 @@
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if stepCaption && me}
|
||||
<div class="step-caption">
|
||||
<span class="step-chip" style:background={playerColor(me.id)}></span>
|
||||
This is you — tap a lit square beside your wizard to walk
|
||||
</div>
|
||||
{/if}
|
||||
<div class="table-stack" class:fpv-primary={prefs.liveFp && !!view.you && !net.spectating}>
|
||||
{#if prefs.liveFp && view.you && !net.spectating}
|
||||
<LiveFirstPerson {view} batch={fpBatch} onhide={() => setPref("liveFp", false)}
|
||||
@@ -2712,7 +2726,6 @@
|
||||
onEdgePeek={(tip) => (peekEdge = tip)}
|
||||
{litCells}
|
||||
spotCells={stepCells}
|
||||
spotCaption={stepCaption}
|
||||
{sightTrace}
|
||||
povFacing={prefs.liveFp && view.you && !net.spectating ? fpvFacing : null}
|
||||
povCreatureId={prefs.liveFp && !net.spectating ? fpvBody : null}
|
||||
@@ -3487,8 +3500,31 @@
|
||||
margin: 0.2rem 0 0.1rem;
|
||||
}
|
||||
/* The eyes toggle floats over the board's corner so it costs the maze no height. */
|
||||
.board-zone { position: relative; }
|
||||
.board-zone { position: relative; flex-direction: column; align-items: stretch; }
|
||||
.view-toggle { position: absolute; top: 0.25rem; right: 0.25rem; z-index: 3; }
|
||||
.step-caption {
|
||||
align-self: flex-start;
|
||||
max-width: calc(100% - 11rem);
|
||||
margin: 0 0 0.4rem 0.4rem;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(28, 22, 12, 0.92);
|
||||
border: 1.5px solid #f2cf5b;
|
||||
color: #f7ecc8;
|
||||
font-family: "Oswald", sans-serif;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.02em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
.step-chip {
|
||||
display: inline-block;
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
border-radius: 2px;
|
||||
border: 1.5px solid #f7ecc8;
|
||||
}
|
||||
.boxlid-tag {
|
||||
font-size: 0.95rem;
|
||||
color: #6b5a41;
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
markedCells = null,
|
||||
litCells = null,
|
||||
spotCells = [],
|
||||
spotCaption = null,
|
||||
markedSector = null,
|
||||
ghostSlots = null,
|
||||
onGhostClick,
|
||||
@@ -70,8 +69,6 @@
|
||||
litCells?: Set<string> | null;
|
||||
/** The step light's squares: a pulsing rim on each. */
|
||||
spotCells?: string[];
|
||||
/** One line hung above a square — the first stride's cue. */
|
||||
spotCaption?: { cell: { x: number; y: number }; text: string } | null;
|
||||
/** Origin of a picked-up 5x5 sector: the whole sector outlines as taken. */
|
||||
markedSector?: { x: number; y: number } | null;
|
||||
/** Empty 5x5 slot origins a sector may land on — drawn as dashed ground
|
||||
@@ -697,17 +694,6 @@
|
||||
{@const sy = Number(key.split(",")[1])}
|
||||
<rect x={sx * CELL + 3} y={sy * CELL + 3} width={CELL - 6} height={CELL - 6} class="spot-cell" rx="5" />
|
||||
{/each}
|
||||
{#if spotCaption}
|
||||
{@const cols = Math.max(...Object.keys(view.board.cells).map((k) => Number(k.split(",")[0]))) + 1}
|
||||
{@const cw = spotCaption.text.length * 6.6 + 20}
|
||||
{@const cx = Math.min(Math.max(spotCaption.cell.x * CELL + CELL / 2, cw / 2 + 4), cols * CELL - cw / 2 - 4)}
|
||||
{@const above = spotCaption.cell.y > 0}
|
||||
{@const cy = above ? spotCaption.cell.y * CELL - 14 : (spotCaption.cell.y + 1) * CELL + 14}
|
||||
<g class="spot-caption" transform={`translate(${cx}, ${cy})`}>
|
||||
<rect x={-cw / 2} y={-12} width={cw} height={24} rx="12" />
|
||||
<text x="0" y="4">{spotCaption.text}</text>
|
||||
</g>
|
||||
{/if}
|
||||
<!-- FEAR's bubble: the three-space diamond, through walls, wrapping the rim -->
|
||||
{#each fearAura as key (key)}
|
||||
{@const bx = Number(key.split(",")[0])}
|
||||
@@ -865,15 +851,6 @@
|
||||
0%, 100% { opacity: 0.35; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
.spot-caption { pointer-events: none; }
|
||||
.spot-caption rect { fill: rgba(28, 22, 12, 0.92); stroke: #f2cf5b; stroke-width: 1.5; }
|
||||
.spot-caption text {
|
||||
fill: #f7ecc8;
|
||||
font-size: 12px;
|
||||
font-family: "Oswald", "Helvetica Neue", Arial, sans-serif;
|
||||
letter-spacing: 0.02em;
|
||||
text-anchor: middle;
|
||||
}
|
||||
.marked-cell {
|
||||
fill: rgba(211, 133, 43, 0.18);
|
||||
stroke: #d3852b;
|
||||
|
||||
Reference in New Issue
Block a user