From 043a8b94a2cfd80261f2b51e31ae6104d7222a71 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Thu, 27 Aug 2026 15:13:46 -0400 Subject: [PATCH] Bodies sharing your square get a tray of their own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A wizard or creature standing IN your square lives inside the renderer's near plane — no sprite, nothing to click, though same-square is exactly where punches, thieves, and a mount's claws operate. A "sharing your square" tray now joins the cockpit above the feet tray: co-located bodies as clickable portraits, routed through the same target handler a sprite click uses — so a possessed skeleton claws its square-mate and a wizard punches theirs, all from inside the pane. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/web/src/LiveFirstPerson.svelte | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/web/src/LiveFirstPerson.svelte b/packages/web/src/LiveFirstPerson.svelte index 1841c7f..e556119 100644 --- a/packages/web/src/LiveFirstPerson.svelte +++ b/packages/web/src/LiveFirstPerson.svelte @@ -113,6 +113,27 @@ cam.x = mx; cam.y = my; cam.facing = face; }); + /** 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. */ + const withYou = $derived.by(() => { + const body = possessed ?? me; + if (!body || !canStride) return []; + const bx = body.position.x, by = body.position.y; + const items: { kind: "player" | "creature"; id: string; art: string; label: string }[] = []; + for (const p of view.players) { + if (!p.alive || p.id === view.you) continue; + if (p.position.x !== bx || p.position.y !== by) continue; + items.push({ kind: "player", id: p.id, art: tokenArt(`wizard-${p.colorIndex}`, "players"), label: p.id }); + } + for (const c of view.creatures) { + if (c.id === possessedId) continue; + if (c.position.x !== bx || c.position.y !== by) continue; + items.push({ kind: "creature", id: c.id, art: tokenArt(c.kind, "creatures"), label: c.kind.replace(/-/g, " ") }); + } + return items; + }); + const cam = $state({ x: 0, y: 0, facing: 0 }); let camReady = false; /** While the player steers, the director keeps its hands off the @@ -409,6 +430,17 @@ {/each} {/if} + {#if withYou.length > 0} +
+ sharing your square + {#each withYou as body (body.id)} + + {/each} +
+ {/if} {#if onpickup && canStride && atFeet.length > 0 && !possessed}
at your feet @@ -499,6 +531,20 @@ color: #c9a72a; } .ride-stats { font-size: 0.7rem; color: #8d8672; } + .with-tray { + position: absolute; + bottom: 54px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 6px; + background: rgba(13, 12, 18, 0.78); + border: 1px solid #3a3428; + border-radius: 6px; + padding: 4px 10px; + z-index: 3; + } .feet-tray { position: absolute; bottom: 8px;