Ambushed teleports carry their destination; room 59UN unstuck (rev 35)
An ambush springing TELEPORT OPPONENT opened a stack with params null —
the spring bypasses cast validation and nothing ever asked where the
victim goes — and resolution crashed on the missing cell, wedging the
room 'waiting on Automaton'. Three layers:
- Crash guards: teleport-opponent and mental-force fizzle gracefully on
a destination-less stack (ungated: no stored command had resolved one).
- The trap commits its destination when laid: setAmbush carries a cell
('wherever you say', said in advance), the spring passes it into the
stack, and the client's arming flow asks for the click.
- Rev 35 refuses arming those spells without a destination; older
ledgers armed blind and their springs fizzle.
All ledgers verified before deploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
co-authored by
Claude Fable 5
parent
e8c25ddc19
commit
76dd019d5d
@@ -321,6 +321,7 @@
|
||||
ambushVia = null;
|
||||
ambushTrigger = null;
|
||||
ambushSpell = null;
|
||||
ambushCell = null;
|
||||
trapCells = [];
|
||||
tradeFrom = null;
|
||||
selectedCreature = null;
|
||||
@@ -499,14 +500,21 @@
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
/** Cell-needing ambush spells choose their destination when laid. */
|
||||
const ambushNeedsCell = $derived(
|
||||
ambushSpell != null &&
|
||||
(ambushSpell.cardId === "teleport-opponent" || ambushSpell.cardId === "mental-force"));
|
||||
let ambushCell = $state<{ x: number; y: number } | null>(null);
|
||||
function armAmbush() {
|
||||
if (!ambushVia || !ambushTrigger || !ambushSpell) return;
|
||||
if (ambushNeedsCell && !ambushCell) return;
|
||||
dispatch({
|
||||
type: "setAmbush",
|
||||
instanceId: ambushVia.instanceId,
|
||||
trigger: { kind: ambushTrigger },
|
||||
spellInstanceId: ambushSpell.instanceId,
|
||||
...(attachedNumber ? { numberInstanceIds: [attachedNumber.instanceId] } : {}),
|
||||
...(ambushCell ? { cell: ambushCell } : {}),
|
||||
});
|
||||
clearSelection();
|
||||
}
|
||||
@@ -519,6 +527,12 @@
|
||||
|
||||
function clickCell(cell: { x: number; y: number }) {
|
||||
if (!view) return;
|
||||
// Laying an ambush with a cell-needing spell: this click is the trap's
|
||||
// destination, chosen in advance ("wherever you say").
|
||||
if (ambushSpell && ambushNeedsCell && !ambushCell) {
|
||||
ambushCell = cell;
|
||||
return;
|
||||
}
|
||||
if (youMustRespond && selectedCard?.cardId === "teleport") {
|
||||
// Mark first, jump on the second tap: an escape spent on a misclick
|
||||
// is an escape wasted.
|
||||
@@ -1887,8 +1901,10 @@
|
||||
<button class="stamp tiny" onclick={() => (ambushTrigger = "treasure")}>grabs a treasure</button>
|
||||
{:else if !ambushSpell}
|
||||
<span>— now tap the attack card to commit</span>
|
||||
{:else if ambushNeedsCell && !ambushCell}
|
||||
<span>{cardDef(ambushSpell.cardId).name} — click where the trap will send them</span>
|
||||
{:else}
|
||||
<span>{cardDef(ambushSpell.cardId).name}{attachedNumber ? ` with a ${numberTotal}` : " (tap a number to power it)"}</span>
|
||||
<span>{cardDef(ambushSpell.cardId).name}{ambushCell ? ` → (${ambushCell.x}, ${ambushCell.y})` : ""}{attachedNumber ? ` with a ${numberTotal}` : " (tap a number to power it)"}</span>
|
||||
<button class="stamp tiny" onclick={armAmbush}>Arm the ambush</button>
|
||||
{/if}
|
||||
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
||||
|
||||
@@ -136,7 +136,7 @@ class LocalGame {
|
||||
seed,
|
||||
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
||||
...(colors ? { colors } : {}),
|
||||
deckRev: 34,
|
||||
deckRev: 35,
|
||||
};
|
||||
const { state, events } = createGame(config);
|
||||
for (const e of events) {
|
||||
|
||||
Reference in New Issue
Block a user