diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index ec4b547..d365406 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -971,8 +971,10 @@ if (view && me) { const left = view.turn.movementAllowance - view.turn.movementUsed; // Unspent legs are no regret when the walk ended ON someone's gold — - // stopping there was the point. - if (left > 0 && !treasuresHere.some((t) => t.owner !== view.you)) { + // stopping there was the point — nor when a pickup or drop already + // forfeited them: those points cannot be spent, only mourned. + if (left > 0 && !view.turn.actionsEnded && + !treasuresHere.some((t) => t.owner !== view.you)) { reasons.push(`${left} movement point${left === 1 ? "" : "s"} unspent`); } const limit = me.displayed.some((c) => c.cardId === "brainstone") ? 9 : 7; @@ -1901,7 +1903,10 @@ {#if prefs.liveFp && view.you && !net.spectating} setPref("liveFp", false)} onstride={(side) => tryMove(side)} canStride={yourMoment} - ontarget={fpvTarget} onfacing={(s) => (fpvFacing = s)} {litCells} /> + ontarget={fpvTarget} onfacing={(s) => (fpvFacing = s)} {litCells} + onpickup={(w) => dispatch(w.kind === "treasure" + ? { type: "pickUpTreasure", treasureId: w.id } + : { type: "pickUpObject", instanceId: w.id })} /> {/if} void) | null; /** Cell-card eligibility, shared with the board's dimming aid. */ litCells?: Set | null; + /** 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; } = $props(); const povId = $derived(view.you); const me = $derived(view.players.find((p) => p.id === povId && p.alive) ?? null); + /** What lies in YOUR square, grabbable: floor treasures and dropped + * objects. The raycaster has no downward pitch — the tray is the eye's + * substitute for looking at your own boots. */ + const atFeet = $derived.by(() => { + if (!me || me.carriedTreasureId) return []; + const k = `${me.position.x},${me.position.y}`; + const items: { kind: "treasure" | "object"; id: string; art: string | null; label: string }[] = []; + for (const t of view.treasures) { + if (!t.position || t.carriedBy) continue; + if (`${t.position.x},${t.position.y}` !== k) continue; + if (t.owner === view.you && k === `${me.home.x},${me.home.y}`) continue; // safe at home + const ci = view.players.find((p) => p.id === t.owner)?.colorIndex ?? 0; + items.push({ kind: "treasure", id: t.id, art: tokenArt(`treasure-${ci % 6}`, "objects"), label: `${t.owner}'s treasure` }); + } + for (const o of view.groundObjects[k] ?? []) { + const file = objectArt(o.cardId); + items.push({ kind: "object", id: o.instanceId, art: file ? tokenArt(file, "objects") : null, label: o.cardId.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 @@ -261,6 +287,21 @@
← → turn · ↑ ↓ walk
{/if} + {#if onpickup && canStride && atFeet.length > 0} +
+ at your feet + {#each atFeet as item (item.id)} + + {/each} +
+ {/if} {/if} @@ -314,6 +355,38 @@ .drive-left { left: 0; top: 15%; bottom: 15%; width: 13%; } .drive-right { right: 0; top: 15%; bottom: 15%; width: 13%; } .drive-right:hover { background: linear-gradient(to left, rgba(0, 0, 0, 0.22), transparent); } + .feet-tray { + position: absolute; + bottom: 8px; + 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-label { + font-family: "Oswald", sans-serif; + font-size: 0.6rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #8d8672; + } + .feet-item { + background: none; + border: 1px solid #4a4536; + border-radius: 4px; + padding: 2px; + cursor: pointer; + line-height: 0; + } + .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; } .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%; }