Illusion walls shimmer until YOU test them (rules rev 29)
Per the owner's design — and truer to a real table, where the cast is public: every illusion wall's location is open knowledge, rendered as a shimmering wall to any player whose eyes have not yet ruled on it. Testing is an explicit act: tap the shimmer (or bump into it) and a modal explains the 50/50 and offers 'Roll to test your eyes' — the new testIllusion command rolls once, then the wall either hardens or dissolves for you alone. Nobody's verdict is moved by watching someone else stroll through (the Q3WZ mystery: an automaton walking through its own round-1 fake). Untested walls block movement and sight without consuming RNG; the pre-29 lazy belief roll is preserved for stored games. Automatons doubt free shimmers on their best crossing before spending any card on that wall. 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
500d9a7154
commit
d5b2b92f2c
@@ -10,7 +10,7 @@
|
||||
import { scheduleFx, type BoardFx } from "./fx";
|
||||
import { CREATURE_ART, objectArt, TERRAIN_ART, tokenArt } from "./art";
|
||||
import { local } from "./local.svelte";
|
||||
import { allCardDefs, cardDef, isNumberCard, isPermanentDuration, SIDES, stepTarget, cellKey, isMovableObject, sightedCellsFor, stackSightTrace, type GameView, eligibleCellsFor } from "@wizwar/engine";
|
||||
import { allCardDefs, cardDef, isNumberCard, isPermanentDuration, SIDES, stepTarget, cellKey, edgeKey, isMovableObject, sightedCellsFor, stackSightTrace, type GameView, eligibleCellsFor } from "@wizwar/engine";
|
||||
import type { CardInstance, Side } from "@wizwar/engine";
|
||||
|
||||
net.connect();
|
||||
@@ -637,11 +637,17 @@
|
||||
}
|
||||
}
|
||||
// Adjacent but blocked? Send the move anyway — unlocked doors, mist-body
|
||||
// and pass-through-wall are resolved server-side.
|
||||
// and pass-through-wall are resolved server-side. A shimmering illusion
|
||||
// in the way asks for the belief test first instead.
|
||||
for (const side of SIDES) {
|
||||
const n = { x: me.position.x + (side === "E" ? 1 : side === "W" ? -1 : 0),
|
||||
y: me.position.y + (side === "S" ? 1 : side === "N" ? -1 : 0) };
|
||||
if (cellKey(n) === cellKey(cell)) {
|
||||
const k = edgeKey(me.position, side);
|
||||
if (view.illusionEdges[k] === "untested") {
|
||||
illusionPrompt = illusionEdgeParts(k);
|
||||
return;
|
||||
}
|
||||
dispatch({ type: "move", direction: side });
|
||||
return;
|
||||
}
|
||||
@@ -930,6 +936,19 @@
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
// A shimmering illusion wall, tapped or bumped: offer the belief test.
|
||||
let illusionPrompt = $state<{ cell: { x: number; y: number }; side: Side } | null>(null);
|
||||
function illusionEdgeParts(key: string): { cell: { x: number; y: number }; side: Side } {
|
||||
const [kind, coords] = key.split(":") as [string, string];
|
||||
const [x, y] = coords.split(",").map(Number) as [number, number];
|
||||
return { cell: { x, y }, side: kind === "V" ? "E" : "S" };
|
||||
}
|
||||
function rollIllusion() {
|
||||
if (!illusionPrompt) return;
|
||||
dispatch({ type: "testIllusion", cell: illusionPrompt.cell, side: illusionPrompt.side });
|
||||
illusionPrompt = null;
|
||||
}
|
||||
|
||||
let pickChoice = $state(false);
|
||||
function pickUp() {
|
||||
if (treasuresHere.length > 1) { pickChoice = true; return; }
|
||||
@@ -1119,6 +1138,30 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if illusionPrompt}
|
||||
<div class="scrim attack-scrim" role="button" tabindex="-1"
|
||||
onclick={() => (illusionPrompt = null)} onkeydown={() => {}}>
|
||||
<div class="attack-notice" role="dialog" aria-label="a shimmering wall" tabindex="-1"
|
||||
onclick={(ev) => ev.stopPropagation()} onkeydown={() => {}}>
|
||||
<div class="attack-title">That wall shimmers strangely…</div>
|
||||
<div class="attack-power">
|
||||
Someone conjured an ILLUSION WALL here. Whether YOUR eyes are fooled
|
||||
is a 50/50 roll — once. Believe it, and it is stone to you until
|
||||
dispelled; see through it, and you may walk it like open floor.
|
||||
Other wizards' eyes rule for themselves.
|
||||
</div>
|
||||
<div class="attack-actions">
|
||||
{#if isYourTurn}
|
||||
<button class="stamp primary" onclick={rollIllusion}>Roll to test your eyes</button>
|
||||
{:else}
|
||||
<span class="attack-power">— you can test it on your turn —</span>
|
||||
{/if}
|
||||
<button class="hint-cancel" onclick={() => (illusionPrompt = null)}>leave it be</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if faqCardId}
|
||||
<Faq cardId={faqCardId} onclose={() => (faqCardId = null)} />
|
||||
{/if}
|
||||
@@ -1503,6 +1546,7 @@
|
||||
ghostSlots={relocateGhosts}
|
||||
onGhostClick={clickGhostSlot}
|
||||
onCellPeek={(c) => peekAt(c)}
|
||||
onIllusionClick={(cell, side) => (illusionPrompt = { cell, side })}
|
||||
{litCells}
|
||||
{sightTrace}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user