Edge cards light their targets; own casts never wrench the neck

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-27 16:08:51 -04:00
co-authored by Claude Fable 5
parent a4a871592b
commit e2dfbd14e3
3 changed files with 29 additions and 8 deletions
+2 -1
View File
@@ -1914,7 +1914,8 @@
onbody={(id) => (fpvBody = id)}
onWarpStep={(creatureId) => dispatch(creatureId
? { type: "creatureWarpStep", creatureId }
: { type: "warpStep" })} />
: { type: "warpStep" })}
edgeSelect={edgeSelectMode && yourMoment} />
{/if}
<Board
{view}
+14 -4
View File
@@ -17,7 +17,7 @@
import { untrack } from "svelte";
import type { GameEvent, GameView, Side } from "@wizwar/engine";
let { view, batch, onhide, onstride = null, canStride = false, ontarget = null, onfacing = null, litCells = null, onpickup = null, onCreatureMove = null, onCreatureAttack = null, onbody = null, onWarpStep = null }: {
let { view, batch, onhide, onstride = null, canStride = false, ontarget = null, onfacing = null, litCells = null, edgeSelect = false, onpickup = null, onCreatureMove = null, onCreatureAttack = null, onbody = null, onWarpStep = null }: {
view: GameView;
/** The latest live event batch, numbered so each plays once, with
* the view the server sent alongside it. */
@@ -34,6 +34,8 @@
onfacing?: ((side: Side) => void) | null;
/** Cell-card eligibility, shared with the board's dimming aid. */
litCells?: Set<string> | 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 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, Math.atan2(ay - ty, ax - tx));
const sight = castRay(v, tx, ty, wantFacing);
if (sight.warped || sight.dist < toAim - 0.4) return takeCutaway(ax, ay);
targetFacing = Math.atan2(ay - ty, ax - tx);
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 @@
<FirstPerson {view} povId={cutawayShot ? "" : (possessedId ?? povId)}
x={cam.x} y={cam.y} facing={cam.facing} width={960} height={400}
fx={fpFx} posOverride={actorPos}
ontarget={handleTarget} {litCells} />
ontarget={handleTarget} {litCells} {edgeSelect} />
<button class="drive" onclick={() => manualTurn(1)} aria-label="turn right"></button>
</div>
<div class="bezel-bottom">
+10
View File
@@ -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<string> | 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.