From e2dfbd14e30145414036a44258773b17a61c066e Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Thu, 27 Aug 2026 16:08:51 -0400 Subject: [PATCH] Edge cards light their targets; own casts never wrench the neck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raising an edge-target card now makes every wall and door face breathe faint gold in the pane — the cockpit's answer to the board's edge handles — with hover still brightening the chosen face. And the director's own-magic aim gains a quarter-turn cap: a deed you aimed behind you via the keymap stays behind you, while deeds done TO you may still spin the world. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/web/src/App.svelte | 3 ++- packages/web/src/LiveFirstPerson.svelte | 24 +++++++++++++++++------- packages/web/src/fpv/FirstPerson.svelte | 10 ++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 10fe2b8..2e8aa5a 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1914,7 +1914,8 @@ onbody={(id) => (fpvBody = id)} onWarpStep={(creatureId) => dispatch(creatureId ? { type: "creatureWarpStep", creatureId } - : { type: "warpStep" })} /> + : { type: "warpStep" })} + edgeSelect={edgeSelectMode && yourMoment} /> {/if} void) | null; /** Cell-card eligibility, shared with the board's dimming aid. */ litCells?: Set | null; + /** An edge-target card is raised: wall faces offer themselves. */ + edgeSelect?: boolean; /** The at-your-feet tray's grab: the pane cannot look down, so what * lies in your own square shows as clickable icons instead. */ onpickup?: ((w: { kind: "treasure" | "object"; id: string }) => void) | null; @@ -346,11 +348,19 @@ return takeCutaway(m.position.x + 0.5, m.position.y + 0.5); } else if (aim && !aim.self) { const ax = aim.x + 0.5, ay = aim.y + 0.5; - const toAim = Math.hypot(ax - tx, ay - ty); - const sight = castRay(v, tx, ty, Math.atan2(ay - ty, ax - tx)); - if (sight.warped || sight.dist < toAim - 0.4) return takeCutaway(ax, ay); - targetFacing = Math.atan2(ay - ty, ax - tx); - aimed = true; + const wantFacing = Math.atan2(ay - ty, ax - tx); + // Your own magic turns your head — but never wrenches it: a deed + // you aimed BEHIND you via the keymap stays behind you. Things + // done TO you may still spin the world. + if (actor === povId && Math.abs(shortestArc(camF, wantFacing)) > Math.PI * 0.55) { + // deliberately unaimed + } else { + const toAim = Math.hypot(ax - tx, ay - ty); + const sight = castRay(v, tx, ty, wantFacing); + if (sight.warped || sight.dist < toAim - 0.4) return takeCutaway(ax, ay); + targetFacing = wantFacing; + aimed = true; + } } else if (performance.now() >= manualUntil) { // Another wizard acting in your sight line turns your head. const other = actor !== povId ? v.players.find((p) => p.id === actor && p.alive) : null; @@ -455,7 +465,7 @@ + ontarget={handleTarget} {litCells} {edgeSelect} />
diff --git a/packages/web/src/fpv/FirstPerson.svelte b/packages/web/src/fpv/FirstPerson.svelte index 531f4d4..4721ddd 100644 --- a/packages/web/src/fpv/FirstPerson.svelte +++ b/packages/web/src/fpv/FirstPerson.svelte @@ -24,6 +24,7 @@ rubble = [], ontarget, litCells = null, + edgeSelect = false, }: { view: GameView; povId: string; @@ -45,6 +46,9 @@ /** Squares a selected cell-target card may aim at: the pane dims the * ineligible ground exactly as the board dims its squares. */ litCells?: Set | null; + /** An edge-target card is raised: every wall and door face glows + * faintly, the pane's answer to the board's edge handles. */ + edgeSelect?: boolean; } = $props(); const FOV = Math.PI / 2.9; @@ -386,6 +390,12 @@ ctx.fillStyle = `rgba(120,70,220,${haze})`; ctx.fillRect(col, top, 1, wallH); } + if (edgeSelect && !hit.frame && (hit.kind === "wall" || hit.kind === "door") && hit.edge) { + // The raised edge-card's invitation: faces breathe gold. + const offer = 0.10 + 0.05 * Math.sin(time / 420 + hit.worldU * 3); + ctx.fillStyle = `rgba(201,167,42,${Math.max(0, offer)})`; + ctx.fillRect(col, top, 1, wallH); + } } // Sprites, far to near, sliced against the depth buffer.