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)} onbody={(id) => (fpvBody = id)}
onWarpStep={(creatureId) => dispatch(creatureId onWarpStep={(creatureId) => dispatch(creatureId
? { type: "creatureWarpStep", creatureId } ? { type: "creatureWarpStep", creatureId }
: { type: "warpStep" })} : { type: "warpStep" })} />
targeting={!!selectedCard && yourMoment} />
{/if} {/if}
<Board <Board
{view} {view}
+66 -114
View File
@@ -17,7 +17,7 @@
import { untrack } from "svelte"; import { untrack } from "svelte";
import type { GameEvent, GameView, Side } from "@wizwar/engine"; 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; view: GameView;
/** The latest live event batch, numbered so each plays once, with /** The latest live event batch, numbered so each plays once, with
* the view the server sent alongside it. */ * the view the server sent alongside it. */
@@ -46,9 +46,6 @@
/** A dimensional-warp step for whichever body the pane is: the /** A dimensional-warp step for whichever body the pane is: the
* wizard (null) or the ridden creature (its id). */ * wizard (null) or the ridden creature (its id). */
onWarpStep?: ((creatureId: string | null) => void) | null; 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(); } = $props();
const povId = $derived(view.you); const povId = $derived(view.you);
@@ -427,36 +424,22 @@
{#if me} {#if me}
<div class="live-fp"> <div class="live-fp">
<FirstPerson {view} povId={cutawayShot ? "" : (possessedId ?? povId)} <!-- The bezel: instruments frame the windshield instead of covering
x={cam.x} y={cam.y} facing={cam.facing} width={960} height={400} it — every canvas pixel stays target glass. -->
fx={fpFx} posOverride={actorPos} <div class="bezel-top">
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} {#if possessed}
<div class="ride-banner">
<span class="ride-label">riding the {possessed.kind.replace(/-/g, " ")}</span> <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> <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> <button class="stamp tiny" onclick={dismount}>back to your eyes</button>
</div>
{:else if rideable.length > 0 && (onCreatureMove || onCreatureAttack)} {:else if rideable.length > 0 && (onCreatureMove || onCreatureAttack)}
<div class="ride-row">
{#each rideable as c (c.id)} {#each rideable as c (c.id)}
<button class="stamp tiny" onclick={() => { possessedId = c.id; manualUntil = performance.now() + 60000; }}> <button class="stamp tiny" onclick={() => { possessedId = c.id; manualUntil = performance.now() + 60000; }}>
👁 ride the {c.kind.replace(/-/g, " ")} 👁 ride the {c.kind.replace(/-/g, " ")}
</button> </button>
{/each} {/each}
</div>
{/if} {/if}
<span class="bezel-spacer"></span>
{#if withYou.length > 0} {#if withYou.length > 0}
<div class="with-tray">
<span class="feet-label">sharing your square</span> <span class="feet-label">sharing your square</span>
{#each withYou as body (body.id)} {#each withYou as body (body.id)}
<button class="feet-item" title={body.label} <button class="feet-item" title={body.label}
@@ -464,10 +447,23 @@
<img src={body.art} alt={body.label} /> <img src={body.art} alt={body.label} />
</button> </button>
{/each} {/each}
{/if}
<button class="live-fp-hide" onclick={onhide} title="hide (re-enable in preferences)"></button>
</div> </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}
{#if canStride && ((!possessed && atFeet.length > 0) || underfoot.hazard || underfoot.warpHere)} {#if canStride && ((!possessed && atFeet.length > 0) || underfoot.hazard || underfoot.warpHere)}
<div class="feet-tray">
<span class="feet-label">at your feet</span> <span class="feet-label">at your feet</span>
{#if !possessed && onpickup} {#if !possessed && onpickup}
{#each atFeet as item (item.id)} {#each atFeet as item (item.id)}
@@ -487,74 +483,63 @@
{#if underfoot.warpHere && onWarpStep} {#if underfoot.warpHere && onWarpStep}
<button class="stamp tiny" onclick={() => onWarpStep?.(possessedId)}> step through the warp</button> <button class="stamp tiny" onclick={() => onWarpStep?.(possessedId)}> step through the warp</button>
{/if} {/if}
</div>
{/if} {/if}
<span class="bezel-spacer"></span>
<span class="live-fp-hint">← → turn · ↑ ↓ walk</span>
</div>
</div> </div>
{/if} {/if}
<style> <style>
.live-fp { .live-fp {
position: relative;
margin-bottom: 0.6rem; margin-bottom: 0.6rem;
border: 1px solid #3a3428;
border-radius: 6px;
background: #14121a;
overflow: hidden;
} }
.live-fp-hide { .bezel-top, .bezel-bottom {
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%);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
background: rgba(13, 12, 18, 0.78); padding: 4px 8px;
border: 1px solid #3a3428; min-height: 34px;
border-radius: 6px; }
padding: 4px 10px; .bezel-top { border-bottom: 1px solid #2a2620; }
z-index: 3; .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 { .ride-label {
font-family: "Oswald", sans-serif; font-family: "Oswald", sans-serif;
@@ -564,34 +549,6 @@
color: #c9a72a; color: #c9a72a;
} }
.ride-stats { font-size: 0.7rem; color: #8d8672; } .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 { .feet-label {
font-family: "Oswald", sans-serif; font-family: "Oswald", sans-serif;
font-size: 0.6rem; font-size: 0.6rem;
@@ -608,12 +565,7 @@
line-height: 0; line-height: 0;
} }
.feet-item:hover { border-color: #c9a72a; } .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-fallback { font-size: 0.65rem; color: #d8d2c0; padding: 0 4px; }
.feet-hazard { font-size: 0.7rem; color: #d98a4a; white-space: nowrap; } .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> </style>