From 728f9fc28a6569ce31efbc44b2a629b13d92b80e Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Thu, 27 Aug 2026 15:19:22 -0400 Subject: [PATCH] The feet tray learns hazards and the warp step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/web/src/App.svelte | 5 ++- packages/web/src/LiveFirstPerson.svelte | 55 +++++++++++++++++++------ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 1626f48..2bfe587 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -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} 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 = { + 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} {/if} - {#if onpickup && canStride && atFeet.length > 0 && !possessed} + {#if canStride && ((!possessed && atFeet.length > 0) || underfoot.hazard || underfoot.warpHere)}
at your feet - {#each atFeet as item (item.id)} - - {/each} + {#if !possessed && onpickup} + {#each atFeet as item (item.id)} + + {/each} + {/if} + {#if underfoot.hazard} + ⚠ {underfoot.hazard} + {/if} + {#if underfoot.warpHere && onWarpStep} + + {/if}
{/if} @@ -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%; }