From 04dc9bc750359216c9ac9e43cf29381be47e156e Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 21:49:27 -0400 Subject: [PATCH] Teleport waits for a second tap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One tap cast the jump, and a player reaching for click-per-square movement spent their teleport on the adjacent floor. Now the first tap only marks the destination (the familiar pulsing square), and the jump commits on a second tap of the same square or the hint-bar button — with the hint spelling out the four-square, walls-ignored range. The escape teleport gets the same two-tap arming; a counter wasted on a misclick is a counter wasted. Co-Authored-By: Claude Fable 5 --- packages/web/src/App.svelte | 43 +++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 27a8db5..b2f6172 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -95,6 +95,8 @@ let rotateCW = $state(true); /** mega-monster: which stat the chosen monster doubles. */ let megaBoost = $state<"life" | "movement">("life"); + /** teleport: the marked destination awaiting its confirming second tap. */ + let pendingTeleport = $state<{ x: number; y: number } | null>(null); /** Cards marked for discard. */ let discardSelection = $state>(new Set()); /** Voluntary discard mode: hand clicks mark cards instead of playing them. */ @@ -171,6 +173,16 @@ return out; }); + function confirmTeleport() { + if (!selectedCard || !pendingTeleport) return; + dispatch({ + type: "cast", instanceId: selectedCard.instanceId, + target: { kind: "cell", cell: pendingTeleport }, + ...(attachedNumber ? { numberInstanceIds: [attachedNumber.instanceId] } : {}), + }); + clearSelection(); + } + function clickGhostSlot(origin: { x: number; y: number }) { if (!selectedCard || !pendingSectorFrom) return; dispatch({ @@ -237,6 +249,7 @@ selectedCreature = null; selectedCard = null; megaBoost = "life"; + pendingTeleport = null; attachedNumber = null; attachedMods = []; wbDamage = 0; @@ -419,6 +432,12 @@ function clickCell(cell: { x: number; y: number }) { if (!view) return; if (youMustRespond && selectedCard?.cardId === "teleport") { + // Mark first, jump on the second tap: an escape spent on a misclick + // is an escape wasted. + if (!pendingTeleport || cellKey(pendingTeleport) !== cellKey(cell)) { + pendingTeleport = cell; + return; + } dispatch({ type: "counteract", instanceId: selectedCard.instanceId, params: { cell } }); clearSelection(); return; @@ -434,6 +453,16 @@ clearSelection(); return; } + if (selectedCard?.cardId === "teleport") { + // Teleport is one jump, not a walk: the first tap only marks the + // destination, the second (or the hint-bar button) commits it. + if (!pendingTeleport || cellKey(pendingTeleport) !== cellKey(cell)) { + pendingTeleport = cell; + return; + } + confirmTeleport(); + return; + } if (selectedCard?.cardId === "relocate-sector") { // Any board click (re-)picks the sector; the landing is chosen from the // dashed ghost slots beyond the maze, since every on-board slot is taken. @@ -1194,7 +1223,7 @@ onPlayerClick={clickPlayer} onCreatureClick={clickCreature} onWarpClick={clickWarp} - markedCell={tradeFrom} + markedCell={tradeFrom ?? pendingTeleport} markedSector={pendingSectorOrigin} ghostSlots={relocateGhosts} onGhostClick={clickGhostSlot} @@ -1381,7 +1410,9 @@ {#if youMustRespond && view.stack} {#if view.stack.defenderId === view.you && selectedCard?.cardId === "teleport"} - Escaping by teleport — click a square within four spaces. + {pendingTeleport + ? "Escaping by teleport — tap the marked square again to jump." + : "Escaping by teleport — tap a square within four spaces to mark it."} {:else if view.stack.defenderId === view.you} {view.stack.attackerId} attacks with {attackingCreature ? `the ${cardDef(attackingCreature.kind).name}` : view.stack.attackCard ? cardDef(view.stack.attackCard.cardId).name : "a punch"} — @@ -1422,6 +1453,14 @@ {:else if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)} {tradeFrom ? "— now the second square" : "— click the first square"} {/if} + {#if selectedCard?.cardId === "teleport" && !youMustRespond} + {pendingTeleport + ? "— tap the marked square again to jump, or pick another" + : "— tap a destination (up to four squares, walls ignored)"} + {#if pendingTeleport} + + {/if} + {/if} {#if selectedCard?.cardId === "mega-monster"} — double its