Rules rev 3: Ward is a choice, Chaos honors the shield
WARD's text says "you may play at that time" — under rules revision 3 the choice is made by setting the trap: an armWard command toggles it on your own turn (your secret; a private wardSet event and a rail note), and the spring fires only while set, consuming the card and the arming together. Async games keep their agency without an interrupt window, exactly as the ambush system solved this before. CHAOS gains its printed interactions: "FULL SHIELD removes a player from participation" — a defender's shield sits them out instead of stopping the spell, and after the counter chain every bystander gets a shield window in seat order (chaosPending; queue head owes a response in lobby summaries, the rail says whose hand hangs in the balance) before the pile forms among the unshielded. "REFLECTIONS have no effect" — both reflections are refused as counters. ABSORB SPELL still eats the whole thing through the existing absorb path. Both changes are frozen behind deckRev 3 so every stored game — and the live one — replays byte-for-byte under its own rules. Five new tests cover armed/unarmed wards, shielded defenders and bystanders, the reflection refusal, and the legacy auto-ward. Not deployed — a live game is in progress. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
41c384274d
commit
7e3e00bf97
@@ -87,6 +87,8 @@
|
||||
: null,
|
||||
);
|
||||
const youMustDiscard = $derived(view != null && view.pendingDiscard === view.you);
|
||||
const youMustShield = $derived(view?.chaosPending?.queue[0] === view?.you && view != null);
|
||||
const holdingWard = $derived(view?.yourHand.some((c) => c.cardId === "ward") ?? false);
|
||||
|
||||
const selectedDef = $derived(selectedCard ? cardDef(selectedCard.cardId) : null);
|
||||
const EDGE_CARDS = new Set([
|
||||
@@ -181,7 +183,7 @@
|
||||
if (!view) return;
|
||||
peekCard = null;
|
||||
peekCreatureId = null;
|
||||
if (youMustRespond) {
|
||||
if (youMustRespond || youMustShield) {
|
||||
dispatch({ type: "counteract", instanceId: card.instanceId });
|
||||
return;
|
||||
}
|
||||
@@ -1003,6 +1005,17 @@
|
||||
<div class="slip">{view.activePlayerId} is taking their turn…</div>
|
||||
{/if}
|
||||
|
||||
{#if view.yourWardArmed}
|
||||
<div class="slip ambush-note">🗡 Your ward is set — a thief who grabs your treasure bleeds for 3.</div>
|
||||
{/if}
|
||||
{#if youMustShield}
|
||||
<div class="slip urgent">
|
||||
Chaos comes for your hand — tap your Full Shield to sit out, or
|
||||
<button class="stamp tiny" onclick={pass}>let it take you</button>
|
||||
</div>
|
||||
{:else if view.chaosPending}
|
||||
<div class="slip">Chaos gathers — waiting on {view.chaosPending.queue[0]}…</div>
|
||||
{/if}
|
||||
{#each idleCreatures as c (c.id)}
|
||||
{@const moves = c.movesPerTurn - c.movementUsed}
|
||||
<div class="slip creature-note">
|
||||
@@ -1197,6 +1210,10 @@
|
||||
onclick={() => dispatch({ type: "pickUpObject", instanceId: obj.instanceId })}>
|
||||
Pick up {cardDef(obj.cardId).name}</button>
|
||||
{/each}
|
||||
{#if holdingWard}
|
||||
<button class="stamp" onclick={() => dispatch({ type: "armWard", armed: !view.yourWardArmed })}>
|
||||
{view.yourWardArmed ? "Stand down the ward" : "Set the ward"}</button>
|
||||
{/if}
|
||||
<button class="stamp" class:primary={punchWallMode} disabled={view.turn.attackUsed && !punchWallMode}
|
||||
onclick={() => (punchWallMode = !punchWallMode)}>
|
||||
{punchWallMode ? "Click the wall to punch — or cancel" : "Punch a wall…"}</button>
|
||||
|
||||
@@ -101,7 +101,7 @@ class LocalGame {
|
||||
seed,
|
||||
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
||||
...(colors ? { colors } : {}),
|
||||
deckRev: 2,
|
||||
deckRev: 3,
|
||||
};
|
||||
const { state, events } = createGame(config);
|
||||
this.config = config;
|
||||
|
||||
@@ -129,6 +129,8 @@ export function humanize(e: GameEvent): string | null {
|
||||
case "treasurePickedUp": return `${e.player} grabs ${e.owner}'s treasure!`;
|
||||
case "objectDropped": return `${e.player} sets down the ${cardDef(e.card.cardId).name}${e.forced ? " (forced)" : ""}.`;
|
||||
case "objectPickedUp": return `${e.player} picks up the ${cardDef(e.card.cardId).name} — actions over.`;
|
||||
case "wardSet": return e.armed ? "Your ward is set — the next thief bleeds." : "Your ward stands down.";
|
||||
case "chaosShielded": return `${e.player} raises a FULL SHIELD and sits out the chaos.`;
|
||||
case "wallDamaged": {
|
||||
const what = e.needed === 15 ? "door" : "wall";
|
||||
return `${e.player} batters the ${what} with ${e.source === "punch" ? "bare fists" : cardDef(e.source).name} — ${e.total}/${e.needed}.`;
|
||||
|
||||
Reference in New Issue
Block a user