The instruments move into the bezel; the windshield goes bare

Per the table's suggestion: chevrons and trays no longer overlay the
canvas — a frame holds them. The top bezel carries the ride controls,
the sharing-your-square portraits, and the hide button; tall turn
paddles flank the glass; the bottom bezel holds the strides, the
at-your-feet tray, hazard chips, the warp-step, and the key hint.
Every canvas pixel is now permanently hover-and-click target glass,
and the yield-while-targeting hack retires unneeded.

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:23:14 -04:00
co-authored by Claude Fable 5
parent 0530ecb039
commit 7bd8eff11c
2 changed files with 75 additions and 124 deletions
+1 -2
View File
@@ -1914,8 +1914,7 @@
onbody={(id) => (fpvBody = id)}
onWarpStep={(creatureId) => dispatch(creatureId
? { type: "creatureWarpStep", creatureId }
: { type: "warpStep" })}
targeting={!!selectedCard && yourMoment} />
: { type: "warpStep" })} />
{/if}
<Board
{view}
+74 -122
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, targeting = false }: {
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. */
@@ -46,9 +46,6 @@
/** 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;
/** A card is aimed: the drive strips yield the glass so every pixel
* hovers and clicks as a target (keys still steer). */
targeting?: boolean;
} = $props();
const povId = $derived(view.you);
@@ -427,36 +424,22 @@
{#if me}
<div class="live-fp">
<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} />
<button class="live-fp-hide" onclick={onhide} title="hide (re-enable in preferences)"></button>
<!-- The helm, tappable: edge strips turn and stride. -->
<button class="drive drive-left" class:yielded={targeting} onclick={() => manualTurn(-1)} aria-label="turn left"></button>
<button class="drive drive-right" class:yielded={targeting} onclick={() => manualTurn(1)} aria-label="turn right"></button>
{#if canStride}
<button class="drive drive-fwd" class:yielded={targeting} onclick={() => manualStride(false)} aria-label="step forward">︿</button>
<button class="drive drive-back" class:yielded={targeting} onclick={() => manualStride(true)} aria-label="step back"></button>
<div class="live-fp-hint">← → turn · ↑ ↓ walk</div>
{/if}
{#if possessed}
<div class="ride-banner">
<!-- The bezel: instruments frame the windshield instead of covering
it — every canvas pixel stays target glass. -->
<div class="bezel-top">
{#if possessed}
<span class="ride-label">riding the {possessed.kind.replace(/-/g, " ")}</span>
<span class="ride-stats">{Math.max(0, possessed.movesPerTurn - possessed.movementUsed)} moves{possessed.attackUsed ? "" : " · claws ready"}</span>
<button class="stamp tiny" onclick={dismount}>back to your eyes</button>
</div>
{:else if rideable.length > 0 && (onCreatureMove || onCreatureAttack)}
<div class="ride-row">
{:else if rideable.length > 0 && (onCreatureMove || onCreatureAttack)}
{#each rideable as c (c.id)}
<button class="stamp tiny" onclick={() => { possessedId = c.id; manualUntil = performance.now() + 60000; }}>
👁 ride the {c.kind.replace(/-/g, " ")}
</button>
{/each}
</div>
{/if}
{#if withYou.length > 0}
<div class="with-tray">
{/if}
<span class="bezel-spacer"></span>
{#if withYou.length > 0}
<span class="feet-label">sharing your square</span>
{#each withYou as body (body.id)}
<button class="feet-item" title={body.label}
@@ -464,10 +447,23 @@
<img src={body.art} alt={body.label} />
</button>
{/each}
</div>
{/if}
{#if canStride && ((!possessed && atFeet.length > 0) || underfoot.hazard || underfoot.warpHere)}
<div class="feet-tray">
{/if}
<button class="live-fp-hide" onclick={onhide} title="hide (re-enable in preferences)"></button>
</div>
<div class="bezel-mid">
<button class="drive" onclick={() => manualTurn(-1)} aria-label="turn left"></button>
<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} />
<button class="drive" onclick={() => manualTurn(1)} aria-label="turn right"></button>
</div>
<div class="bezel-bottom">
{#if canStride}
<button class="drive stride" onclick={() => manualStride(false)} aria-label="step forward">︿ forward</button>
<button class="drive stride" onclick={() => manualStride(true)} aria-label="step back">﹀ back</button>
{/if}
{#if canStride && ((!possessed && atFeet.length > 0) || underfoot.hazard || underfoot.warpHere)}
<span class="feet-label">at your feet</span>
{#if !possessed && onpickup}
{#each atFeet as item (item.id)}
@@ -487,74 +483,63 @@
{#if underfoot.warpHere && onWarpStep}
<button class="stamp tiny" onclick={() => onWarpStep?.(possessedId)}> step through the warp</button>
{/if}
</div>
{/if}
{/if}
<span class="bezel-spacer"></span>
<span class="live-fp-hint">← → turn · ↑ ↓ walk</span>
</div>
</div>
{/if}
<style>
.live-fp {
position: relative;
margin-bottom: 0.6rem;
border: 1px solid #3a3428;
border-radius: 6px;
background: #14121a;
overflow: hidden;
}
.live-fp-hide {
position: absolute;
top: 0.4rem;
right: 0.4rem;
background: rgba(13, 12, 18, 0.65);
border: 1px solid #5a5342;
border-radius: 3px;
color: #a49c86;
cursor: pointer;
font-size: 0.75rem;
padding: 0.1rem 0.4rem;
}
.live-fp-hide:hover { color: #e9e1cb; }
.live-fp-hint {
position: absolute;
bottom: 0.45rem;
left: 0.6rem;
color: rgba(233, 225, 203, 0.55);
font-family: "Courier Prime", monospace;
font-size: 0.7rem;
pointer-events: none;
}
/* The helm: faint chevron strips along the pane's edges — always
* there for a thumb, louder under a pointer. */
.drive {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
color: rgba(233, 225, 203, 0.35);
font-size: 2.2rem;
line-height: 1;
padding: 0;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
}
.drive:hover {
color: rgba(233, 225, 203, 0.95);
background: linear-gradient(to right, rgba(0, 0, 0, 0.22), transparent);
}
.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); }
.ride-row, .ride-banner {
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
.bezel-top, .bezel-bottom {
display: flex;
align-items: center;
gap: 8px;
background: rgba(13, 12, 18, 0.78);
border: 1px solid #3a3428;
border-radius: 6px;
padding: 4px 10px;
z-index: 3;
padding: 4px 8px;
min-height: 34px;
}
.bezel-top { border-bottom: 1px solid #2a2620; }
.bezel-bottom { border-top: 1px solid #2a2620; }
.bezel-spacer { flex: 1; }
.bezel-mid {
display: flex;
align-items: stretch;
}
.bezel-mid :global(.fpv-canvas) {
border: none;
border-radius: 0;
flex: 1;
min-width: 0;
}
.drive {
background: none;
border: none;
color: #8d8672;
font-size: 1.3rem;
cursor: pointer;
padding: 0 10px;
}
.drive:hover { color: #e9e1cb; background: rgba(201, 167, 42, 0.08); }
.stride { font-size: 0.8rem; letter-spacing: 0.06em; }
.live-fp-hide {
background: none;
border: none;
color: #8d8672;
cursor: pointer;
font-size: 0.9rem;
}
.live-fp-hide:hover { color: #e9e1cb; }
.live-fp-hint {
font-family: "Courier Prime", monospace;
font-size: 0.65rem;
color: #5d5748;
}
.ride-label {
font-family: "Oswald", sans-serif;
@@ -564,34 +549,6 @@
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;
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;
@@ -608,12 +565,7 @@
line-height: 0;
}
.feet-item:hover { border-color: #c9a72a; }
.feet-item img { width: 30px; height: 30px; object-fit: contain; }
.feet-item img { width: 26px; height: 26px; 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; }
.yielded { pointer-events: none; opacity: 0.12; }
.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%; }
.drive-back:hover { background: linear-gradient(to top, rgba(0, 0, 0, 0.22), transparent); }
</style>