From 358633022c9016d678f52b8f454a33ca84c2c8a3 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Thu, 27 Aug 2026 17:41:52 -0400 Subject: [PATCH] Attachments ride every targeted cast, not just player targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine learned to bend neutral spells, but the client only sent aroundCornerInstanceId (and amplify/add/extend) on player-target casts — applyMods was never called on the cell, edge, creature, two-cell, trap, or teleport-opponent paths, so a SAFE aimed around the corner went out bare and the engine rightly refused it. All targeted cast dispatches now pass through withMods, so what the hand shows attached is what the command carries. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/web/src/App.svelte | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 875311a..ce5cd8d 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -423,6 +423,12 @@ } } + /** Every targeted cast goes out through here so attachments never drop. */ + function withMods[0] & { type: "cast" }>(cmd: T): T { + applyMods(cmd); + return cmd; + } + /** Suggest-as-you-type pool for the card-naming field, tuned per card. */ const nameSuggestions = $derived.by(() => { if (!selectedCard) return []; @@ -622,11 +628,11 @@ if (!yourMoment) { peekAt(cell); return; } if (pendingCellFor && selectedCard) { // Stage 2 of teleport-opponent: destination chosen. - dispatch({ + dispatch(withMods({ type: "cast", instanceId: selectedCard.instanceId, target: { kind: "player", playerId: pendingCellFor }, params: { cell }, - }); + })); clearSelection(); return; } @@ -653,17 +659,17 @@ ? trapCells.filter((_, i) => i !== already) : [...trapCells, cell]; if (trapCells.length === 4) { - dispatch({ type: "cast", instanceId: selectedCard.instanceId, params: { cells: trapCells } }); + dispatch(withMods({ type: "cast", instanceId: selectedCard.instanceId, params: { cells: trapCells } })); clearSelection(); } return; } if (selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)) { if (!tradeFrom) { tradeFrom = cell; return; } - dispatch({ + dispatch(withMods({ type: "cast", instanceId: selectedCard.instanceId, target: { kind: "cell", cell }, params: { cell: tradeFrom }, - }); + })); clearSelection(); return; } @@ -676,11 +682,11 @@ return; } if (selectedCard && CELL_CARDS.has(selectedCard.cardId)) { - dispatch({ + dispatch(withMods({ type: "cast", instanceId: selectedCard.instanceId, target: { kind: "cell", cell }, ...(attachedNumber ? { numberInstanceIds: [attachedNumber.instanceId] } : {}), - }); + })); clearSelection(); return; } @@ -858,14 +864,14 @@ if (!selectedCard || !edgeSelectMode) return; // The attached number rides along for every edge cast: wand charges, // wall-of-fire durations, wall attacks alike. - dispatch({ + dispatch(withMods({ type: "cast", instanceId: selectedCard.instanceId, target: { kind: "edge", cell, side }, ...(holdDoor && (selectedCard.cardId === "pick-lock" || selectedCard.cardId === "master-key") ? { params: { hold: true } } : {}), ...(attachedNumber ? { numberInstanceIds: [attachedNumber.instanceId] } : {}), - }); + })); clearSelection(); } @@ -881,12 +887,12 @@ return; } if (selectedCard && (CREATURE_TARGET_CARDS.has(selectedCard.cardId) || cardDef(selectedCard.cardId).cardType === "attack")) { - dispatch({ + dispatch(withMods({ type: "cast", instanceId: selectedCard.instanceId, target: { kind: "creature", creatureId }, ...(selectedCard.cardId === "mega-monster" ? { params: { boost: megaBoost } } : {}), ...(attachedNumber ? { numberInstanceIds: [attachedNumber.instanceId] } : {}), - }); + })); clearSelection(); return; }