The full telepath audit: every private glimpse held up, every silence voiced

A sweep of all 142 engine events against the client found three more
telepath-class drops and eight silent chronicle gaps. THOUGHT STEAL's
plunder and a kill's hand-spoils now get the reveal modal (generalized
from the mind-read one — title, note, the cards fanned out); your own
end-of-turn draw names its cards in the log. The mute events speak:
dispels, walls of fire rising/burning/guttering, filled squares,
crashing waterwalls, clattering thrown objects, and REUSE SPELL's
retrieval. Also verified clean: every param-taking card has collection
UI, and every cell/edge-targeting card is aimable on the board.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-23 11:53:17 -04:00
co-authored by Claude Fable 5
parent 6165d43639
commit 813f92d419
2 changed files with 44 additions and 12 deletions
+33 -12
View File
@@ -54,9 +54,10 @@
let trapNotice = $state<string | null>(null); let trapNotice = $state<string | null>(null);
/** A Ward sprung on YOU earns the same courtesy. */ /** A Ward sprung on YOU earns the same courtesy. */
let wardBite = $state<{ owner: string; amount: number } | null>(null); let wardBite = $state<{ owner: string; amount: number } | null>(null);
/** A mind YOU read: the stolen glimpse arrives once, in an event — hold /** Cards revealed to YOU alone (a read mind, plunder, spoils): the
* it up until dismissed, or it is lost to the scrollback. */ * glimpse arrives once, in an event — hold it up until dismissed, or it
let mindRead = $state<{ player: string; cards: CardInstance[] } | null>(null); * is lost to the scrollback. */
let cardReveal = $state<{ title: string; note: string; cards: CardInstance[] } | null>(null);
/** An easy-to-regret play awaiting its "are you sure?" (see prefs.cautions). */ /** An easy-to-regret play awaiting its "are you sure?" (see prefs.cautions). */
let caution = $state<{ reasons: string[]; go: () => void } | null>(null); let caution = $state<{ reasons: string[]; go: () => void } | null>(null);
function withCaution(reasons: string[], go: () => void) { function withCaution(reasons: string[], go: () => void) {
@@ -96,7 +97,27 @@
for (const e of events) { for (const e of events) {
if (e.type === "trapSprung" && e.player === view.you && e.cardId) trapNotice = e.cardId; if (e.type === "trapSprung" && e.player === view.you && e.cardId) trapNotice = e.cardId;
if (e.type === "handRevealedPrivate" && e.visibleTo === view.you) { if (e.type === "handRevealedPrivate" && e.visibleTo === view.you) {
mindRead = { player: e.player, cards: e.cards }; cardReveal = {
title: `${e.player}'s mind, laid bare`,
note: "Commit them to memory — the glimpse does not linger.",
cards: e.cards,
};
}
if (e.type === "cardsStolenPrivate" && e.visibleTo === view.you) {
const pub = events.find((s) => s.type === "cardsStolen" && s.to === view!.you);
cardReveal = {
title: pub?.type === "cardsStolen" ? `Plucked from ${pub.from}'s thoughts` : "Plucked from their thoughts",
note: "Yours now — already in your hand.",
cards: e.cards,
};
}
if (e.type === "handTakenPrivate" && e.visibleTo === view.you) {
const pub = events.find((s) => s.type === "handTaken" && s.to === view!.you);
cardReveal = {
title: pub?.type === "handTaken" ? `The spoils of ${pub.from}` : "The spoils",
note: "The fallen wizard's hand — already in yours.",
cards: e.cards,
};
} }
if (e.type === "wardSprung" && e.victim === view.you) { if (e.type === "wardSprung" && e.victim === view.you) {
const dmg = events.find((d) => d.type === "damaged" && d.player === e.victim && d.source === "warded treasure"); const dmg = events.find((d) => d.type === "damaged" && d.player === e.victim && d.source === "warded treasure");
@@ -1444,24 +1465,24 @@
</div> </div>
{/if} {/if}
{#if mindRead} {#if cardReveal}
<div class="scrim attack-scrim" role="alertdialog" aria-label="a mind read"> <div class="scrim attack-scrim" role="alertdialog" aria-label="cards revealed to you">
<div class="attack-notice mind-read"> <div class="attack-notice mind-read">
<div class="attack-headline">{mindRead.player}'s mind, laid bare</div> <div class="attack-headline">{cardReveal.title}</div>
<div class="mind-cards"> <div class="mind-cards">
{#each mindRead.cards as card (card.instanceId)} {#each cardReveal.cards as card (card.instanceId)}
<Card {card} onfaq={(id) => (faqCardId = id)} /> <Card {card} onfaq={(id) => (faqCardId = id)} />
{/each} {/each}
{#if mindRead.cards.length === 0} {#if cardReveal.cards.length === 0}
<div class="attack-power">Nothing — their thoughts are empty-handed.</div> <div class="attack-power">Nothing — their thoughts are empty-handed.</div>
{/if} {/if}
</div> </div>
<div class="attack-power"> <div class="attack-power">
{mindRead.cards.length > 0 {cardReveal.cards.length > 0
? `${mindRead.cards.length} card${mindRead.cards.length === 1 ? "" : "s"} commit them to memory; the glimpse does not linger.` ? `${cardReveal.cards.length} card${cardReveal.cards.length === 1 ? "" : "s"} ${cardReveal.note}`
: ""} : ""}
</div> </div>
<button class="stamp big" onclick={() => (mindRead = null)}>Enough</button> <button class="stamp big" onclick={() => (cardReveal = null)}>Enough</button>
</div> </div>
</div> </div>
{/if} {/if}
+11
View File
@@ -88,6 +88,17 @@ export function humanize(e: GameEvent): string | null {
: `${e.player} wasn't holding that card — the erasure fizzles.`; : `${e.player} wasn't holding that card — the erasure fizzles.`;
case "cardsStolen": return `${e.to} steals ${e.count} card(s) from ${e.from}'s thoughts!`; case "cardsStolen": return `${e.to} steals ${e.count} card(s) from ${e.from}'s thoughts!`;
case "handRevealed": return `${e.to} reads ${e.player}'s mind — their hand is revealed.`; case "handRevealed": return `${e.to} reads ${e.player}'s mind — their hand is revealed.`;
case "cardsDrawnPrivate": return e.cards.length > 0
? `You draw ${e.cards.map((c) => cardDef(c.cardId).name).join(", ")}.`
: null;
case "creationDispelled": return `The ${e.what.replace(/-/g, " ")} unravels — dispelled!`;
case "firewallCreated": return `A wall of fire roars up (${e.turns} turn${e.turns === 1 ? "" : "s"})!`;
case "firewallExpired": return `The wall of fire gutters out.`;
case "firewallBurned": return `${e.player} steps through the flames!`;
case "squareFilled": return `The square fills with ${e.kind === "stone" ? "solid stone" : e.kind === "slime" ? "green slime" : e.kind.replace(/-/g, " ")}!`;
case "waterwallCrashes": return `A wall of water crashes down!`;
case "objectThrown": return `The ${cardDef(e.cardId).name.toLowerCase()} clatters to the floor.`;
case "spellReused": return `${e.player}'s ${cardDef(e.card.cardId).name} leaps back to their hand!`;
case "doorUnlocked": return `${e.player} unlocks a door.`; case "doorUnlocked": return `${e.player} unlocks a door.`;
case "doorHeld": return `${e.player} holds the door open.`; case "doorHeld": return `${e.player} holds the door open.`;
case "doorReleased": return `The held door swings shut.`; case "doorReleased": return `The held door swings shut.`;