The door can be held open, as both key cards always promised

MASTER KEY and PICK LOCK each read: "You may 'hold the door open' for
others, if you wish" — and the engine always slammed it at end of
turn. Now the cast takes a hold param: the door stays unlocked past
the turn, for anyone, as long as its holder stands adjacent and
alive. A step away, a shove, a teleport, or a killing blow lets it
swing shut — swept after every command, since anything can move a
wizard. New state, new param: no old ledger contains either, so no
rev gate is needed.

The client offers a "hold the door open" checkbox when either card is
selected; a held door shows pale with a green jamb ("held open by a
standing wizard"), and the chronicle records the holding and the
shutting. Pinned: a held door outlives the turn and admits the other
wizard; walking away releases it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 13:15:57 -04:00
co-authored by Claude Fable 5
parent d2b40ec6f8
commit a0c65413ef
6 changed files with 122 additions and 3 deletions
+8
View File
@@ -128,6 +128,8 @@
let tradeFrom = $state<{ x: number; y: number } | null>(null);
/** rotate-sector direction. */
let rotateCW = $state(true);
/** pick-lock / master-key: prop the door for others once it opens. */
let holdDoor = $state(false);
/** mega-monster: which stat the chosen monster doubles. */
let megaBoost = $state<"life" | "movement">("life");
/** teleport: the marked destination awaiting its confirming second tap. */
@@ -300,6 +302,7 @@
selectedCreature = null;
selectedCard = null;
megaBoost = "life";
holdDoor = false;
pendingTeleport = null;
attachedNumber = null;
attachedMods = [];
@@ -713,6 +716,8 @@
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();
@@ -1550,6 +1555,9 @@
onclick={() => (megaBoost = "movement")}>movement</button>
<span> then tap the monster</span>
{/if}
{#if selectedCard?.cardId === "pick-lock" || selectedCard?.cardId === "master-key"}
<label class="inline"><input type="checkbox" bind:checked={holdDoor} /> hold the door open</label>
{/if}
{#if selectedCard?.cardId === "rotate-sector"}
<label class="inline"><input type="checkbox" bind:checked={rotateCW} /> clockwise</label>
<span> click the sector</span>
+6 -1
View File
@@ -130,7 +130,9 @@
// A door's lock can be gone for good, jammed shut, or picked open
// for the turn — each earns its own look.
const lock = state === "door"
? (view.doorStates[key] ?? (view.openDoorEdges.includes(key) ? "ajar" : null))
? (view.doorStates[key] ??
(view.heldDoorEdges.includes(key) ? "held"
: view.openDoorEdges.includes(key) ? "ajar" : null))
: null;
return { kind, x, y, state, lock };
}),
@@ -363,6 +365,7 @@
{@const cls = e.state === "door" ? `door${e.lock ? ` ${e.lock}` : ""}` : e.state === "firewall" ? "firewall" : "wall"}
{@const lockTitle = e.lock === "removed" ? "lock removed — swings free"
: e.lock === "jammed" ? "lock jammed — sealed for good"
: e.lock === "held" ? "held open by a standing wizard"
: e.lock === "ajar" ? "unlocked until end of turn" : null}
{#if e.kind === "V"}
<rect
@@ -635,6 +638,8 @@
.door.jammed { fill: #3a2a18; stroke: #14100b; }
/* Picked or keyed open until end of turn. */
.door.ajar { fill: #d4bd8e; stroke-dasharray: 5 3; }
/* Held open: a wizard stands propping it. */
.door.held { fill: #d4bd8e; stroke: #2e7d32; }
.firewall {
fill: #d0342c;
stroke: #7c1a14;
+2
View File
@@ -74,6 +74,8 @@ export function humanize(e: GameEvent): string | null {
case "cardsStolen": return `${e.to} steals ${e.count} card(s) from ${e.from}'s thoughts!`;
case "handRevealed": return `${e.to} reads ${e.player}'s mind — their hand is revealed.`;
case "doorUnlocked": return `${e.player} unlocks a door.`;
case "doorHeld": return `${e.player} holds the door open.`;
case "doorReleased": return `The held door swings shut.`;
case "doorsRelocked": return `The door swings shut and relocks.`;
case "doorJammed": return `${e.player} jams a door's lock solid.`;
case "lockRemoved": return `${e.player} removes a door's lock for good.`;