Press-and-hold peeks a segment — the tooltip, for hands without a mouse

The same 450ms hold that peeks a square now works on walls, doors, and
fire walls: the segment's full description (state, lock, hitpoints, the
fire's burn warning) rises in the peek scrim. Shimmering illusions keep
their richer tap modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-18 10:40:55 -04:00
co-authored by Claude Fable 5
parent 497c2245a3
commit 0f91d30a3e
4 changed files with 50 additions and 9 deletions
+9 -3
View File
@@ -938,6 +938,9 @@
clearSelection(); clearSelection();
} }
// Press-and-hold on a wall/door/fire segment (touch has no hover).
let peekEdge = $state<string | null>(null);
// A shimmering illusion wall, tapped or bumped: offer the belief test. // A shimmering illusion wall, tapped or bumped: offer the belief test.
let illusionPrompt = $state<{ cell: { x: number; y: number }; side: Side } | null>(null); let illusionPrompt = $state<{ cell: { x: number; y: number }; side: Side } | null>(null);
function illusionEdgeParts(key: string): { cell: { x: number; y: number }; side: Side } { function illusionEdgeParts(key: string): { cell: { x: number; y: number }; side: Side } {
@@ -1130,11 +1133,13 @@
</div> </div>
{/if} {/if}
{#if peekCard || discardPeek || peekTreasure} {#if peekCard || discardPeek || peekTreasure || peekEdge}
<div class="big-peek-scrim" role="button" tabindex="-1" <div class="big-peek-scrim" role="button" tabindex="-1"
onclick={() => { peekCard = null; peekCreatureId = null; peekToken = null; discardPeek = null; peekTreasure = null; }} onkeydown={() => {}}> onclick={() => { peekCard = null; peekCreatureId = null; peekToken = null; discardPeek = null; peekTreasure = null; peekEdge = null; }} onkeydown={() => {}}>
<div class="big-peek"> <div class="big-peek">
{#if peekTreasure} {#if peekEdge}
<div class="peek-note big-peek-note">{peekEdge}</div>
{:else if peekTreasure}
<img class="big-peek-token" src={peekTreasure.src} alt="" /> <img class="big-peek-token" src={peekTreasure.src} alt="" />
<div class="peek-note big-peek-note">{peekTreasure.note}</div> <div class="peek-note big-peek-note">{peekTreasure.note}</div>
{:else} {:else}
@@ -1559,6 +1564,7 @@
onGhostClick={clickGhostSlot} onGhostClick={clickGhostSlot}
onCellPeek={(c) => peekAt(c)} onCellPeek={(c) => peekAt(c)}
onIllusionClick={(cell, side) => (illusionPrompt = { cell, side })} onIllusionClick={(cell, side) => (illusionPrompt = { cell, side })}
onEdgePeek={(tip) => (peekEdge = tip)}
{litCells} {litCells}
{sightTrace} {sightTrace}
/> />
+6 -2
View File
@@ -26,6 +26,7 @@
onWarpClick, onWarpClick,
onCellPeek, onCellPeek,
onIllusionClick, onIllusionClick,
onEdgePeek,
markedCell = null, markedCell = null,
markedCells = null, markedCells = null,
litCells = null, litCells = null,
@@ -47,6 +48,8 @@
onCellPeek?: (cell: { x: number; y: number }) => void; onCellPeek?: (cell: { x: number; y: number }) => void;
/** Tap on a shimmering (untested) illusion wall: offer the belief test. */ /** Tap on a shimmering (untested) illusion wall: offer the belief test. */
onIllusionClick?: (cell: { x: number; y: number }, side: Side) => void; onIllusionClick?: (cell: { x: number; y: number }, side: Side) => void;
/** Press-and-hold a wall/door/fire segment: its tooltip, for touch. */
onEdgePeek?: (tip: string) => void;
/** First square of a two-square spell: marked so the click reads as taken. */ /** First square of a two-square spell: marked so the click reads as taken. */
markedCell?: { x: number; y: number } | null; markedCell?: { x: number; y: number } | null;
/** A multi-square placement in progress (boobytrap tokens), in click /** A multi-square placement in progress (boobytrap tokens), in click
@@ -387,7 +390,7 @@
<!-- walls & doors & firewalls --> <!-- walls & doors & firewalls -->
{#each edges as e (`${e.kind}:${e.x},${e.y}`)} {#each edges as e (`${e.kind}:${e.x},${e.y}`)}
{#if e.state === "firewall"} {#if e.state === "firewall"}
<FirewallEdge x={e.x} y={e.y} kind={e.kind === "V" ? "V" : "H"} /> <FirewallEdge x={e.x} y={e.y} kind={e.kind === "V" ? "V" : "H"} onpeek={onEdgePeek} />
{:else} {:else}
{@const lockTitle = e.lock === "removed" ? "lock removed — swings free" {@const lockTitle = e.lock === "removed" ? "lock removed — swings free"
: e.lock === "jammed" ? "lock jammed — sealed for good" : e.lock === "jammed" ? "lock jammed — sealed for good"
@@ -396,7 +399,8 @@
<EdgeGlyph x={e.x} y={e.y} kind={e.kind === "V" ? "V" : "H"} <EdgeGlyph x={e.x} y={e.y} kind={e.kind === "V" ? "V" : "H"}
state={e.state === "door" ? "door" : "wall"} state={e.state === "door" ? "door" : "wall"}
lock={(e.lock ?? null) as "removed" | "jammed" | "held" | "ajar" | null} lock={(e.lock ?? null) as "removed" | "jammed" | "held" | "ajar" | null}
title={lockTitle} damage={view.wallDamage[`${e.kind}:${e.x},${e.y}`] ?? 0} /> title={lockTitle} damage={view.wallDamage[`${e.kind}:${e.x},${e.y}`] ?? 0}
onpeek={onEdgePeek} />
{/if} {/if}
{/each} {/each}
+16 -1
View File
@@ -2,13 +2,15 @@
// A wall or door on an edge, exactly as the board draws it. Wall-segment // A wall or door on an edge, exactly as the board draws it. Wall-segment
// effects (locks picked, jammed, removed; doors held) are color states of // effects (locks picked, jammed, removed; doors held) are color states of
// this one glyph — the game has never used tokens for them. // this one glyph — the game has never used tokens for them.
let { x, y, kind, state, lock = null, title = null, damage = 0 }: { let { x, y, kind, state, lock = null, title = null, damage = 0, onpeek }: {
x: number; y: number; kind: "V" | "H"; x: number; y: number; kind: "V" | "H";
state: "wall" | "door"; state: "wall" | "door";
lock?: "removed" | "jammed" | "held" | "ajar" | null; lock?: "removed" | "jammed" | "held" | "ajar" | null;
title?: string | null; title?: string | null;
/** Battle damage taken so far (walls fall at 20, doors at 15). */ /** Battle damage taken so far (walls fall at 20, doors at 15). */
damage?: number; damage?: number;
/** Press-and-hold (touch has no hover): report the tooltip's text. */
onpeek?: (tip: string) => void;
} = $props(); } = $props();
const CELL = 48; const CELL = 48;
const WALL = 7; const WALL = 7;
@@ -19,6 +21,17 @@
? `${Math.max(0, needed - damage)} of ${needed} hitpoints left` ? `${Math.max(0, needed - damage)} of ${needed} hitpoints left`
: `${needed} hitpoints`); : `${needed} hitpoints`);
const tip = $derived(`${title ?? (state === "door" ? "a locked door" : "a wall")} — ${hp}`); const tip = $derived(`${title ?? (state === "door" ? "a locked door" : "a wall")} — ${hp}`);
// Press-and-hold peeks the segment, mirroring the board's cell gesture.
let timer: ReturnType<typeof setTimeout> | null = null;
function press() {
if (!onpeek) return;
timer = setTimeout(() => onpeek?.(tip), 450);
}
function release() {
if (timer) clearTimeout(timer);
timer = null;
}
</script> </script>
{#if kind === "V"} {#if kind === "V"}
@@ -26,12 +39,14 @@
x={(x + 1) * CELL - WALL / 2} y={y * CELL - WALL / 2} x={(x + 1) * CELL - WALL / 2} y={y * CELL - WALL / 2}
width={WALL} height={CELL + WALL} width={WALL} height={CELL + WALL}
class={cls} class={cls}
role="img" onpointerdown={press} onpointerup={release} onpointerleave={release}
><title>{tip}</title></rect> ><title>{tip}</title></rect>
{:else} {:else}
<rect <rect
x={x * CELL - WALL / 2} y={(y + 1) * CELL - WALL / 2} x={x * CELL - WALL / 2} y={(y + 1) * CELL - WALL / 2}
width={CELL + WALL} height={WALL} width={CELL + WALL} height={WALL}
class={cls} class={cls}
role="img" onpointerdown={press} onpointerup={release} onpointerleave={release}
><title>{tip}</title></rect> ><title>{tip}</title></rect>
{/if} {/if}
+19 -3
View File
@@ -2,7 +2,21 @@
// A standing WALL OF FIRE: board furniture, not a flourish — it burns for // A standing WALL OF FIRE: board furniture, not a flourish — it burns for
// as long as the spell holds, so the animation is pure CSS on a handful // as long as the spell holds, so the animation is pure CSS on a handful
// of shapes (no turbulence filters; those are budgeted for one-shot fx). // of shapes (no turbulence filters; those are budgeted for one-shot fx).
let { x, y, kind }: { x: number; y: number; kind: "V" | "H" } = $props(); let { x, y, kind, onpeek }: {
x: number; y: number; kind: "V" | "H";
/** Press-and-hold (touch has no hover): report what this fire is. */
onpeek?: (tip: string) => void;
} = $props();
const TIP = "a wall of fire — passing through burns for 4";
let peekTimer: ReturnType<typeof setTimeout> | null = null;
function press() {
if (!onpeek) return;
peekTimer = setTimeout(() => onpeek?.(TIP), 450);
}
function release() {
if (peekTimer) clearTimeout(peekTimer);
peekTimer = null;
}
const uid = $props.id(); const uid = $props.id();
const CELL = 48; const CELL = 48;
const WALL = 7; const WALL = 7;
@@ -42,10 +56,12 @@
<!-- the burning bar itself, seated on the wall line --> <!-- the burning bar itself, seated on the wall line -->
{#if kind === "V"} {#if kind === "V"}
<rect x={-WALL / 2} y={-CELL / 2 - 2} width={WALL} height={CELL + 4} rx="3" <rect x={-WALL / 2} y={-CELL / 2 - 2} width={WALL} height={CELL + 4} rx="3"
fill={`url(#fw-bar-${uid})`} class="fw-bar" /> fill={`url(#fw-bar-${uid})`} class="fw-bar"
role="img" onpointerdown={press} onpointerup={release} onpointerleave={release} />
{:else} {:else}
<rect x={-CELL / 2 - 2} y={-WALL / 2} width={CELL + 4} height={WALL} rx="3" <rect x={-CELL / 2 - 2} y={-WALL / 2} width={CELL + 4} height={WALL} rx="3"
fill={`url(#fw-bar-${uid})`} class="fw-bar" /> fill={`url(#fw-bar-${uid})`} class="fw-bar"
role="img" onpointerdown={press} onpointerup={release} onpointerleave={release} />
{/if} {/if}
<!-- tongues of flame, each licking on its own beat --> <!-- tongues of flame, each licking on its own beat -->
{#each TONGUES as f (f.t)} {#each TONGUES as f (f.t)}