The feet tray learns hazards and the warp step

Two blind spots close. The active body — wizard or ridden mount —
standing on a dimensional-warp token gets a "step through the warp"
button in the feet tray, dispatching warpStep for the wizard and
creatureWarpStep for the mount (the command the RNRX troll report
earned). And whatever you stand IN shows as a warning chip: tacks,
slime, dust, a pit, the ooze — the near plane's last secrets told.

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 15:19:22 -04:00
co-authored by Claude Fable 5
parent 043a8b94a2
commit 728f9fc28a
2 changed files with 47 additions and 13 deletions
+4 -1
View File
@@ -1911,7 +1911,10 @@
: { type: "pickUpObject", instanceId: w.id })}
onCreatureMove={(creatureId, direction) => dispatch({ type: "moveCreature", creatureId, direction })}
onCreatureAttack={(creatureId, targetId) => dispatch({ type: "creatureAttack", creatureId, targetId })}
onbody={(id) => (fpvBody = id)} />
onbody={(id) => (fpvBody = id)}
onWarpStep={(creatureId) => dispatch(creatureId
? { type: "creatureWarpStep", creatureId }
: { type: "warpStep" })} />
{/if}
<Board
{view}
+43 -12
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 }: {
let { view, batch, onhide, onstride = null, canStride = false, ontarget = null, onfacing = null, litCells = null, 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. */
@@ -43,6 +43,9 @@
/** Reports which creature (if any) the pane is riding, so the keymap
* can move the facing ribbon to the mount's token. */
onbody?: ((creatureId: string | null) => void) | null;
/** A dimensional-warp step for whichever body the pane is: the
* wizard (null) or the ridden creature (its id). */
onWarpStep?: ((creatureId: string | null) => void) | null;
} = $props();
const povId = $derived(view.you);
@@ -113,6 +116,25 @@
cam.x = mx; cam.y = my; cam.facing = face;
});
/** What the ACTIVE body stands in or on: hazards it cannot see below
* itself, and the dimensional token it could step through. */
const underfoot = $derived.by(() => {
const body = possessed ?? me;
if (!body || !canStride) return { hazard: null as string | null, warpHere: false };
const k = `${body.position.x},${body.position.y}`;
const HAZARD_NAMES: Record<string, string> = {
tacks: "tacks underfoot", slime: "green slime", dust: "a dust cloud",
pit: "an open pit", ooze: "the killer ooze",
};
const kind = view.squareContents[k]?.kind;
return {
hazard: kind ? HAZARD_NAMES[kind] ?? null : null,
warpHere: view.dimWarps.some((w) =>
(w.a.x === body.position.x && w.a.y === body.position.y) ||
(w.b.x === body.position.x && w.b.y === body.position.y)),
};
});
/** Bodies sharing the active eyes' square: inside the near plane, so
* the renderer cannot show them — the tray stands in for the sprite,
* and clicking routes exactly as clicking the sprite would. */
@@ -441,19 +463,27 @@
{/each}
</div>
{/if}
{#if onpickup && canStride && atFeet.length > 0 && !possessed}
{#if canStride && ((!possessed && atFeet.length > 0) || underfoot.hazard || underfoot.warpHere)}
<div class="feet-tray">
<span class="feet-label">at your feet</span>
{#each atFeet as item (item.id)}
<button class="feet-item" title={item.label}
onclick={() => onpickup?.({ kind: item.kind, id: item.id })}>
{#if item.art}
<img src={item.art} alt={item.label} />
{:else}
<span class="feet-fallback">{item.label}</span>
{/if}
</button>
{/each}
{#if !possessed && onpickup}
{#each atFeet as item (item.id)}
<button class="feet-item" title={item.label}
onclick={() => onpickup?.({ kind: item.kind, id: item.id })}>
{#if item.art}
<img src={item.art} alt={item.label} />
{:else}
<span class="feet-fallback">{item.label}</span>
{/if}
</button>
{/each}
{/if}
{#if underfoot.hazard}
<span class="feet-hazard">{underfoot.hazard}</span>
{/if}
{#if underfoot.warpHere && onWarpStep}
<button class="stamp tiny" onclick={() => onWarpStep?.(possessedId)}> step through the warp</button>
{/if}
</div>
{/if}
</div>
@@ -577,6 +607,7 @@
.feet-item:hover { border-color: #c9a72a; }
.feet-item img { width: 30px; height: 30px; object-fit: contain; }
.feet-fallback { font-size: 0.65rem; color: #d8d2c0; padding: 0 4px; }
.feet-hazard { font-size: 0.7rem; color: #d98a4a; white-space: nowrap; }
.drive-fwd { top: 0; left: 20%; right: 20%; height: 16%; }
.drive-fwd:hover { background: linear-gradient(to bottom, rgba(0, 0, 0, 0.22), transparent); }
.drive-back { bottom: 0; left: 20%; right: 20%; height: 15%; }