From 3f909b485b0062904d058a7bbcf3e51cf3c4b269 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Thu, 3 Sep 2026 09:41:12 -0400 Subject: [PATCH] A sprung Ward is named as one, not as a punch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eric grabbed a warded treasure and took the Ward's three points — but the attack modal showed a bare-knuckled punch, the hint line said the owner "attacks with a punch", and the cast panel called it a physical blow with no card behind it. The engine knew better: the stack has carried sourceName ("warded treasure") since rev 9, and the damage line used it. The client never read it. Now every cardless stack with a name shows that name — and, for the Ward, the Ward card itself — in the modal, the hint line, and the cast panel, with the damage inbound. In the chronicle, a counteraction played against it records "warded treasure" rather than "punch", and an EMPATHY echo of it names the same. Chronicle-only, ungated; all 58 production ledgers replay clean. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG --- packages/engine/src/game.ts | 14 +++++++------- packages/web/src/App.svelte | 21 ++++++++++++++++++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index c7a02f0..30139b0 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -5779,7 +5779,7 @@ function doCounteract( } player.hand.push(attackCard); const events: GameEvent[] = [ - { type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? "punch" }, + { type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? stack.sourceName ?? "punch" }, { type: "attackAbsorbedIntoHand", player: playerId, attackCard }, { type: "attackResolved", attacker: stack.attackerId, defender: stack.defenderId, attackCardId: attackCard.cardId, damageDealt: 0, reflectedDamage: 0, fullyStopped: true, redirected: false }, ]; @@ -5809,7 +5809,7 @@ function doCounteract( return { ok: true, state, - events: [{ type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? "punch" }], + events: [{ type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? stack.sourceName ?? "punch" }], }; } @@ -5821,7 +5821,7 @@ function doCounteract( takeFromHand(player, instanceId); state.discard.push(card); const events: GameEvent[] = [ - { type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? "punch" }, + { type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? stack.sourceName ?? "punch" }, ]; attachSustained(state, events, "invisible", playerId, playerId, duration); stack.counters.push({ player: playerId, card, nullified: false }); @@ -5838,7 +5838,7 @@ function doCounteract( takeFromHand(player, instanceId); state.discard.push(card); const events: GameEvent[] = [ - { type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? "punch" }, + { type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? stack.sourceName ?? "punch" }, ]; attachSustained(state, events, "empathy", playerId, playerId, duration); stack.counters.push({ player: playerId, card, nullified: false }); @@ -5877,7 +5877,7 @@ function doCounteract( return { ok: true, state, - events: [{ type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? "punch" }], + events: [{ type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? stack.sourceName ?? "punch" }], }; } @@ -5897,7 +5897,7 @@ function doCounteract( return { ok: true, state, - events: [{ type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? "punch" }], + events: [{ type: "counteractionPlayed", player: playerId, card, cardId: card.cardId, against: stack.attackCard?.cardId ?? stack.sourceName ?? "punch" }], }; } @@ -6246,7 +6246,7 @@ function resolveStack(state: GameState, events: GameEvent[]): void { // EMPATHY: "Any attack done in any form against you acts against both // you and the caster of the spell." if (damageDealt > 0 && sustainedOn(state, defender.id, "empathy").length > 0 && attacker.alive) { - applyDamage(state, events, attacker, damageDealt, `${attackId ?? "punch"} (empathy)`, defender.id, pipe.kind); + applyDamage(state, events, attacker, damageDealt, `${attackId ?? stack.sourceName ?? "punch"} (empathy)`, defender.id, pipe.kind); } // SHADOWSTONE: physical damage you deal feeds your life total. if (damageDealt > 0 && pipe.kind === "physical" && displays(attacker, "shadowstone") && attacker.alive) { diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 29db92c..9ddd71f 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -230,6 +230,10 @@ let peekToken = $state(null); /** Ledger chest tapped: the treasure full-size, with whose and who-holds. */ let peekTreasure = $state<{ src: string; note: string } | null>(null); + /** Cardless blows that a card nonetheless made: the stack names the + * blow, and this names the card to show for it. */ + const BLOW_CARD: Record = { "warded treasure": "ward" }; + /** Bare-knuckle demolition: click a wall to punch it. */ let punchWallMode = $state(false); /** Leafing through the face-up discard pile. */ @@ -1864,6 +1868,15 @@ {view.stack.amplifyFactor > 1 ? ` — AMPLIFIED ×${view.stack.amplifyFactor}` : ""} {/if} + {:else if view.stack.defenderId === view.you && view.stack.sourceName} + + {#if BLOW_CARD[view.stack.sourceName]} +
(faqCardId = id)} />
+ {/if} +
+ {view.stack.sourceName.toUpperCase()} — {view.stack.params?.damage ?? 0} damage inbound unless you answer it +
{:else if view.stack.defenderId === view.you}
👊 a bare-knuckled punch
{:else if view.stack.counters.length > 0} @@ -2194,7 +2207,8 @@ : view.stack.attackCard?.cardId === "illusionary-attack" && view.stack.params?.cardId ? `an illusionary ${cardDef(view.stack.params.cardId).name}` : view.stack.attackCard ? cardDef(view.stack.attackCard.cardId).name - : view.stack.tearTreasure ? "a grab at your treasure" : "a punch"} — + : view.stack.tearTreasure ? "a grab at your treasure" + : view.stack.sourceName ? `a ${view.stack.sourceName}` : "a punch"} — tap a counteraction card, or {:else} {view.stack.waitingOn === view.you ? "They counter your spell — counter back, or" : ""} @@ -2609,6 +2623,11 @@
⚔ incoming
{#if view.stack.attackCard}
(faqCardId = id)} />
+ {:else if view.stack.sourceName} + {#if BLOW_CARD[view.stack.sourceName]} +
(faqCardId = id)} />
+ {/if} +
{view.stack.sourceName} — it bites for {view.stack.params?.damage ?? 0}
{:else}
a physical blow — no card behind it
{/if}