Credibility pass: the fast-edit shavings swept from the ops-and-rev-13 batch

Server: turnFacts joins actingSeat's full priority order (a hanging
ward or slow-death window now reads as the lobby's attention, stubs
included), summarize and stubOf share one baseSummary, the stranded
doc comment returns to summarize, the protocol contract names
feedbackList, Sentry init moves below the imports it cannot precede,
randomBytes joins its group, the feedback path hoists once with the
operator-fields note. Engine: the safe's sight check trusts castSight
alone, the locked-safe predicate becomes lockedSafeAt, the doomed-grab
test drives both seats and must reach endTurn. Web: the twin safe
handlers merge, paPoints joins the reset ritual with a derived burnable
range, five decorative empty handlers and a dead class go, the hand
earns real list semantics, fitHandCards cites Card.svelte's tokens,
two wrap-sites reindent, feedbackReports moves home as a named type,
and the chronicle's stay-mounted reason is stated. feedback-reply.sh
adopts its siblings' set/usage conventions and speaks Node. Two test
screenshots leave the repo root.

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-09-01 11:04:01 -04:00
co-authored by Claude Fable 5
parent d37bc62c28
commit afa0e17483
11 changed files with 178 additions and 146 deletions
+78 -74
View File
@@ -448,8 +448,8 @@
(selectedCard != null || (youMustRespond && !!view?.stack)));
/** The hand in the player's own order: drag a card onto another to take
* its place. The order lives only in this browser — the server's hand
* array is authority for contents, this for arrangement. */
* its place. The order lives only in this tab, for this sitting — the
* server's hand array is authority for contents, this for arrangement. */
let handOrder = $state<string[]>([]);
let draggingId = $state<string | null>(null);
const orderedHand = $derived.by(() => {
@@ -474,11 +474,14 @@
let handEl = $state<HTMLDivElement | null>(null);
function fitHandCards(el: HTMLDivElement) {
if (!prefs.handLeft) { el.style.removeProperty("--card-zoom"); return; }
const cardW = 8.2 * 16, gap = 0.45 * 16, pad = 0.6 * 16;
// Card.svelte is the source of truth for the card's box: 8.2rem wide,
// 11.4rem tall. 5rem covers the masthead and the column's padding.
const rem = 16;
const cardW = 8.2 * rem, gap = 0.45 * rem, pad = 0.6 * rem;
const zoomW = (el.clientWidth - pad - gap) / (2 * cardW);
const rows = Math.max(1, Math.ceil((view?.yourHand.length ?? 7) / 2));
const cardH = 1.42 * cardW;
const maxH = window.innerHeight - 5 * 16;
const cardH = (11.4 / 8.2) * cardW;
const maxH = window.innerHeight - 5 * rem;
const zoomH = (maxH - pad) / (rows * (cardH + gap));
el.style.setProperty("--card-zoom", String(Math.max(0.8, Math.min(zoomW, zoomH, 1.8)).toFixed(3)));
}
@@ -495,7 +498,8 @@
const el = handEl;
if (el) requestAnimationFrame(() => fitHandCards(el));
});
/** POWER ATTACK: life points burned into the attached attack. */
/** POWER ATTACK: life points burned into the attached attack — always
* fewer than the caster has left. */
let paPoints = $state(1);
const NAMED_CARDS = new Set(["card-erasure", "drop-object", "deja-vu", "thief", "remove-curse", "swarthmores-enchantment", "illusionary-attack"]);
@@ -516,6 +520,7 @@
}
function clearSelection() {
paPoints = 1;
punchWallMode = false;
swapMeetTarget = null;
swapMine = null;
@@ -786,16 +791,11 @@
confirmTeleport();
return;
}
if (selectedCard && (selectedCard.cardId === "pick-lock" || selectedCard.cardId === "master-key") &&
view.squareContents[cellKey(cell)]?.kind === "safe") {
// A key aimed at a SAFE's square opens the box, not a door.
dispatch(withMods({ type: "cast", instanceId: selectedCard.instanceId, target: { kind: "cell", cell } }));
clearSelection();
return;
}
if (selectedCard && cardDef(selectedCard.cardId).cardType === "attack" &&
view.squareContents[cellKey(cell)]?.kind === "safe") {
// Brute force: attacks batter the safe itself (fifteen points bursts it).
// A SAFE takes the cast itself: a key turns its combination, and an
// attack batters the box (fifteen points bursts it).
if (selectedCard && view.squareContents[cellKey(cell)]?.kind === "safe" &&
(selectedCard.cardId === "pick-lock" || selectedCard.cardId === "master-key" ||
cardDef(selectedCard.cardId).cardType === "attack")) {
dispatch(withMods({ type: "cast", instanceId: selectedCard.instanceId, target: { kind: "cell", cell } }));
clearSelection();
return;
@@ -1325,6 +1325,7 @@
function pass() { dispatch({ type: "pass" }); }
const me = $derived(view?.players.find((p) => p.id === view?.you) ?? null);
const paBurnable = $derived(me ? [1, 2, 3, 4, 5].filter((n) => n < me.life) : []);
/** Two treasures can share a square: one dispatches, more ask whose. */
const treasuresHere = $derived(
view && me
@@ -2056,7 +2057,7 @@
{#if net.feedbackReports.length > 0}
<div class="reports-head">your reports to the wizards</div>
{#each net.feedbackReports as r (r.id)}
<div class="report-row" class:answered={!!r.reply}>
<div class="report-row">
<span class="ledger-code">{r.roomId}</span>
<div class="report-body">
<div class="report-text">“{r.happened}”</div>
@@ -2232,8 +2233,8 @@
{#if attachedMods.some((m) => m.cardId === "power-attack")}
<label class="inline">burn
<select class="tier-pick" bind:value={paPoints} aria-label="life points to burn">
{#each [1, 2, 3, 4, 5] as n (n)}
{#if me && n < me.life}<option value={n}>{n}</option>{/if}
{#each paBurnable as n (n)}
<option value={n}>{n}</option>
{/each}
</select>
life for +{paPoints} damage</label>
@@ -2441,14 +2442,14 @@
{:else if view.stack?.defenderId === view.you}
Under attack — respond by your hand.
{#if view.stack.attackCard}
<span class="respond-card"><Card card={view.stack.attackCard} onclick={() => {}} /></span>
<span class="respond-card"><Card card={view.stack.attackCard} /></span>
{/if}
{:else}
Your spell is countered — respond by your hand.
{#if view.stack?.counters.length}
{@const latest = [...view.stack.counters].reverse().find((c) => !c.nullified)}
{#if latest}
<span class="respond-card"><Card card={latest.card} onclick={() => {}} /></span>
<span class="respond-card"><Card card={latest.card} /></span>
{/if}
{/if}
{/if}
@@ -2597,49 +2598,52 @@
</div>
</div>
<!-- The chronicle below stays MOUNTED while the panel covers it
(class-hidden, not unrendered) so its scroll position and
stick-to-newest state survive the casting. -->
{#if castPanelOn && view}
<div class="cast-panel" class:respond={youMustRespond} aria-label={youMustRespond ? "counteraction" : "casting"}>
{#if youMustRespond && view.stack}
<div class="cast-panel-head">⚔ incoming</div>
{#if view.stack.attackCard}
<div class="cast-panel-card"><Card card={view.stack.attackCard} onfaq={(id) => (faqCardId = id)} /></div>
{:else}
<div class="cast-panel-note">a physical blow — no card behind it</div>
{/if}
{#if view.stack.numberValue != null || view.stack.amplifyFactor > 1}
<div class="cast-panel-note">
{view.stack.numberValue != null ? `powered by a ${view.stack.numberValue}` : ""}
{view.stack.amplifyFactor > 1 ? ` amplified ×${view.stack.amplifyFactor}` : ""}
</div>
{/if}
{#if view.stack.counters.length > 0}
<div class="cast-panel-sub">counters so far</div>
<div class="cast-panel-minis">
{#each view.stack.counters as c (c.card.instanceId)}
<span class="cast-mini" class:nullified={c.nullified}>
<Card card={c.card} onclick={() => {}} /></span>
{/each}
</div>
{/if}
{:else if selectedCard}
<div class="cast-panel-card"><Card card={selectedCard} onfaq={(id) => (faqCardId = id)} /></div>
{#if attachedNumber || attachedMods.length > 0}
<div class="cast-panel-sub">riding along</div>
<div class="cast-panel-minis">
<div class="cast-panel" class:respond={youMustRespond} aria-label={youMustRespond ? "counteraction" : "casting"}>
{#if youMustRespond && view.stack}
<div class="cast-panel-head">⚔ incoming</div>
{#if view.stack.attackCard}
<div class="cast-panel-card"><Card card={view.stack.attackCard} onfaq={(id) => (faqCardId = id)} /></div>
{:else}
<div class="cast-panel-note">a physical blow — no card behind it</div>
{/if}
{#if view.stack.numberValue != null || view.stack.amplifyFactor > 1}
<div class="cast-panel-note">
{view.stack.numberValue != null ? `powered by a ${view.stack.numberValue}` : ""}
{view.stack.amplifyFactor > 1 ? ` amplified ×${view.stack.amplifyFactor}` : ""}
</div>
{/if}
{#if view.stack.counters.length > 0}
<div class="cast-panel-sub">counters so far</div>
<div class="cast-panel-minis">
{#each view.stack.counters as c (c.card.instanceId)}
<span class="cast-mini" class:nullified={c.nullified}>
<Card card={c.card} /></span>
{/each}
</div>
{/if}
{:else if selectedCard}
<div class="cast-panel-card"><Card card={selectedCard} onfaq={(id) => (faqCardId = id)} /></div>
{#if attachedNumber || attachedMods.length > 0}
<div class="cast-panel-sub">riding along</div>
<div class="cast-panel-minis">
{#if attachedNumber}
<span class="cast-mini"><Card card={attachedNumber} /></span>
{/if}
{#each attachedMods as m (m.instanceId)}
<span class="cast-mini"><Card card={m} /></span>
{/each}
</div>
{#if attachedNumber}
<span class="cast-mini"><Card card={attachedNumber} onclick={() => {}} /></span>
<div class="cast-panel-note">number total {numberTotal}</div>
{/if}
{#each attachedMods as m (m.instanceId)}
<span class="cast-mini"><Card card={m} onclick={() => {}} /></span>
{/each}
</div>
{#if attachedNumber}
<div class="cast-panel-note">number total {numberTotal}</div>
{/if}
{/if}
{/if}
{@render tableGuidance()}
</div>
{@render tableGuidance()}
</div>
{/if}
<div class="chronicle" class:tucked={castPanelOn} aria-label="game log" bind:this={chronicleEl}
onscroll={onChronicleScroll}>
@@ -2786,7 +2790,7 @@
</div>
{/if}
<div class="hand" bind:this={handEl} class:spent={actionsSpent && !discardMode && discardSelection.size === 0} aria-label="your hand">
<div class="hand" bind:this={handEl} role="list" class:spent={actionsSpent && !discardMode && discardSelection.size === 0} aria-label="your hand">
{#if net.spectating}
<span class="gallery-note">👁 You watch from the Peanut Gallery hands stay secret, even from you.</span>
{/if}
@@ -2798,20 +2802,20 @@
ondragover={(e) => e.preventDefault()}
ondrop={(e) => { e.preventDefault(); dropCardOn(card.instanceId); }}
ondragend={() => (draggingId = null)}>
<Card
{card}
selected={selectedCard?.instanceId === card.instanceId ||
ambushVia?.instanceId === card.instanceId}
attached={attachedNumber?.instanceId === card.instanceId ||
ambushSpell?.instanceId === card.instanceId ||
attachedMods.some((m) => m.instanceId === card.instanceId)}
marked={discardSelection.has(card.instanceId)}
displayed={view.players.find((p) => p.id === view.you)?.displayed.some((c) => c.instanceId === card.instanceId) ?? false}
glowing={interruptCards.some((c) => c.instanceId === card.instanceId)}
charges={view.wandCharges[card.instanceId] ?? null}
onclick={() => selectCard(card)}
onfaq={(id) => (faqCardId = id)}
/>
<Card
{card}
selected={selectedCard?.instanceId === card.instanceId ||
ambushVia?.instanceId === card.instanceId}
attached={attachedNumber?.instanceId === card.instanceId ||
ambushSpell?.instanceId === card.instanceId ||
attachedMods.some((m) => m.instanceId === card.instanceId)}
marked={discardSelection.has(card.instanceId)}
displayed={view.players.find((p) => p.id === view.you)?.displayed.some((c) => c.instanceId === card.instanceId) ?? false}
glowing={interruptCards.some((c) => c.instanceId === card.instanceId)}
charges={view.wandCharges[card.instanceId] ?? null}
onclick={() => selectCard(card)}
onfaq={(id) => (faqCardId = id)}
/>
</div>
{/each}
</div>