The cruel draw gets its modal, and its own words

GIFT FROM BELOW bit for three in the middle of end-of-turn noise and
nobody saw it — not live, not in the replay, where its log line even
claimed "lose a turn" (the other trap's fate). The trapSprung event
now names its card; drawing a trap YOURSELF raises the full modal —
the card, what it did, "Curse the deck" — and the chronicle tells
each trap's true consequence. The amplify narration was already
fixed; replays re-derive events, so both now show in old games'
reels too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 17:13:36 -04:00
co-authored by Claude Fable 5
parent 028ff813f3
commit bed7d849eb
3 changed files with 26 additions and 4 deletions
+3 -3
View File
@@ -602,7 +602,7 @@ export type GameEvent =
| { type: "cardDisplayed"; player: PlayerId; card: CardInstance }
| { type: "extraTurnGranted"; player: PlayerId }
| { type: "lifeTraded"; player: PlayerId; points: number; newAllowance: number }
| { type: "trapSprung"; player: PlayerId }
| { type: "trapSprung"; player: PlayerId; cardId?: string }
| { type: "died"; player: PlayerId; killedBy: PlayerId | null }
| { type: "handTaken"; from: PlayerId; to: PlayerId; count: number }
| { type: "handTakenPrivate"; visibleTo: PlayerId; cards: CardInstance[] }
@@ -5639,13 +5639,13 @@ function doEndTurn(prev: GameState, draw: number): CommandResult {
if (isTrap(card.cardId)) {
state.discard.push(card);
p.lostTurns++;
events.push({ type: "trapSprung", player: p.id });
events.push({ type: "trapSprung", player: p.id, cardId: card.cardId });
continue;
}
if (card.cardId === "gift-from-below") {
// "You lose 3 points to magical damage, now ... then discard and redraw."
state.discard.push(card);
events.push({ type: "trapSprung", player: p.id });
events.push({ type: "trapSprung", player: p.id, cardId: card.cardId });
applyDamage(state, events, p, 3, "gift from below", null);
checkVictory(state, events);
if (!p.alive) break;
+20
View File
@@ -47,8 +47,13 @@
/** Live spell flourishes on the board (cosmetic, self-expiring). */
let boardFx = $state<BoardFx[]>([]);
let fxCancels: (() => void)[] = [];
/** A trap drawn by YOU earns a modal; buried log lines get missed. */
let trapNotice = $state<string | null>(null);
function playFx(events: Parameters<typeof scheduleFx>[0]) {
if (!view) return;
for (const e of events) {
if (e.type === "trapSprung" && e.player === view.you && e.cardId) trapNotice = e.cardId;
}
fxCancels.push(scheduleFx(
events, view,
(fx) => (boardFx = [...boardFx, fx]),
@@ -1029,6 +1034,21 @@
</div>
{/if}
{#if trapNotice}
<div class="scrim attack-scrim" role="alertdialog" aria-label="a trap card">
<div class="attack-notice">
<div class="attack-headline">A cruel draw!</div>
<div class="attack-card"><Card card={{ instanceId: "trap-notice", cardId: trapNotice }} /></div>
<div class="attack-power">
{trapNotice === "gift-from-below"
? "It bites for 3 points, then the deck deals you another."
: "The trap snaps shut — you lose a turn."}
</div>
<button class="stamp big" onclick={() => (trapNotice = null)}>Curse the deck</button>
</div>
</div>
{/if}
{#if view && view.outOfTurnWindow?.playerId === view.you && !momentSeen}
<div class="scrim attack-scrim" role="alertdialog" aria-label="your interruption">
<div class="attack-notice">
+3 -1
View File
@@ -57,7 +57,9 @@ export function humanize(e: GameEvent): string | null {
case "wallCreated": return `A wall appears!`;
case "wallDestroyed": return e.wasDoor ? `A door is blasted to rubble!` : `A wall crumbles!`;
case "extraTurnGranted": return `${e.player} speeds up — extra turn banked.`;
case "trapSprung": return `${e.player} walked into an old TRAP! Lose a turn.`;
case "trapSprung": return e.cardId === "gift-from-below"
? `${e.player} draws GIFT FROM BELOW — it bites for 3, then deals again!`
: `${e.player} walked into an old TRAP! Lose a turn.`;
case "attackMissed": return e.because === "invisible"
? `The attack passes through empty air — ${e.defender} is invisible!`
: `${e.defender} is too small to hit — the attack misses!`;