The table speaks plainer, and keeps its whimsy
Wording throughout the app, the server's messages and the engine's refusals trades its stacked dashes for sentences, while the maze still trusts you to honor its curses, the gallery still listens, and the clockwork still learns its best tricks from the humans at the table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ad23c83b8a
commit
c64ff70ed4
@@ -4,19 +4,19 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wiz-War</title>
|
||||
<meta name="description" content="A game of magical combat in a stone labyrinth. The classic 1993 sixth edition, faithfully reproduced — play free with friends, online or around one device." />
|
||||
<meta name="description" content="A game of magical combat in a stone labyrinth. The classic 1993 sixth edition, faithfully reproduced. Play free online with friends, or pass one device around." />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/favicon.png" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Wiz-War" />
|
||||
<meta property="og:title" content="Wiz-War" />
|
||||
<meta property="og:description" content="A game of magical combat in a stone labyrinth. The classic 1993 sixth edition, faithfully reproduced — play free with friends, online or around one device." />
|
||||
<meta property="og:description" content="A game of magical combat in a stone labyrinth. The classic 1993 sixth edition, faithfully reproduced. Play free online with friends, or pass one device around." />
|
||||
<meta property="og:image" content="https://wizwar.kestrelsnest.social/og.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Wiz-War" />
|
||||
<meta name="twitter:description" content="A game of magical combat in a stone labyrinth. The classic 1993 sixth edition, faithfully reproduced — play free with friends." />
|
||||
<meta name="twitter:description" content="A game of magical combat in a stone labyrinth. The classic 1993 sixth edition, faithfully reproduced. Play free online with friends." />
|
||||
<meta name="twitter:image" content="https://wizwar.kestrelsnest.social/og.png" />
|
||||
<meta name="theme-color" content="#efe8d4" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
|
||||
+187
-187
File diff suppressed because it is too large
Load Diff
@@ -457,10 +457,10 @@
|
||||
{#if e.state === "firewall"}
|
||||
<FirewallEdge x={e.x} y={e.y} kind={e.kind === "V" ? "V" : "H"} onpeek={onEdgePeek} />
|
||||
{:else}
|
||||
{@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}
|
||||
{@const lockTitle = e.lock === "removed" ? "a door with its lock removed, swinging free"
|
||||
: e.lock === "jammed" ? "a door with its lock jammed, sealed for good"
|
||||
: e.lock === "held" ? "a door held open by a standing wizard"
|
||||
: e.lock === "ajar" ? "a door unlocked until the end of the turn" : null}
|
||||
<EdgeGlyph x={e.x} y={e.y} kind={e.kind === "V" ? "V" : "H"}
|
||||
state={e.state === "door" ? "door" : "wall"}
|
||||
lock={(e.lock ?? null) as LockState | null}
|
||||
@@ -479,7 +479,7 @@
|
||||
? edgeScar((wx + 1) * CELL, wy * CELL + 3, (wx + 1) * CELL, (wy + 1) * CELL - 3, frac)
|
||||
: edgeScar(wx * CELL + 3, (wy + 1) * CELL, (wx + 1) * CELL - 3, (wy + 1) * CELL, frac)}
|
||||
<path d={scar} class="crack-shadow" style:opacity={0.35 + frac * 0.65} />
|
||||
<path d={scar} class="crack" style:opacity={0.35 + frac * 0.65}><title>battle-scarred — {dmg} damage taken</title></path>
|
||||
<path d={scar} class="crack" style:opacity={0.35 + frac * 0.65}><title>battle-scarred, {dmg} damage taken</title></path>
|
||||
{/each}
|
||||
|
||||
<!-- illusions YOU know are fake: ghostly dashed lines -->
|
||||
@@ -488,9 +488,9 @@
|
||||
{@const ix = Number(key.split(":")[1]?.split(",")[0])}
|
||||
{@const iy = Number(key.split(":")[1]?.split(",")[1])}
|
||||
{#if kind === "V"}
|
||||
<path d={edgeWisps((ix+1)*CELL, iy*CELL, (ix+1)*CELL, (iy+1)*CELL)} class="illusion"><title>an illusion — your eyes see through it</title></path>
|
||||
<path d={edgeWisps((ix+1)*CELL, iy*CELL, (ix+1)*CELL, (iy+1)*CELL)} class="illusion"><title>an illusion your eyes see through</title></path>
|
||||
{:else}
|
||||
<path d={edgeWisps(ix*CELL, (iy+1)*CELL, (ix+1)*CELL, (iy+1)*CELL)} class="illusion"><title>an illusion — your eyes see through it</title></path>
|
||||
<path d={edgeWisps(ix*CELL, (iy+1)*CELL, (ix+1)*CELL, (iy+1)*CELL)} class="illusion"><title>an illusion your eyes see through</title></path>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
x={-CELL * 0.26} y={-CELL * 0.26}
|
||||
width={CELL * 0.52} height={CELL * 0.52}
|
||||
cls={`token-art hit${c.id === selectedCreatureId ? " selected-art" : ""}`}
|
||||
title={`${c.kind} (${c.controllerId}) — ${c.damage}/${Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg`}
|
||||
title={`${c.kind} (${c.controllerId}), ${c.damage}/${Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg`}
|
||||
/>
|
||||
<rect
|
||||
x={-CELL * 0.26} y={-CELL * 0.26}
|
||||
@@ -666,7 +666,7 @@
|
||||
class:selected={c.id === selectedCreatureId}
|
||||
stroke={playerColor(c.controllerId)}
|
||||
>
|
||||
<title>{c.kind} ({c.controllerId}) — {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg</title>
|
||||
<title>{c.kind} ({c.controllerId}), {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg</title>
|
||||
</rect>
|
||||
<text x="0" y="4" class="creature-label">{c.kind === "fire-imp" ? "I" : c.kind === "democratic-monster" ? "D" : c.kind[0]?.toUpperCase()}</text>
|
||||
{/if}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
const hp = $derived(damage > 0
|
||||
? `${Math.max(0, needed - damage)} of ${needed} points left`
|
||||
: `${needed} points to destroy`);
|
||||
const tip = $derived(`${title ?? (state === "door" ? "a locked door" : "a wall")} — ${hp}`);
|
||||
const tip = $derived(`${title ?? (state === "door" ? "a locked door" : "a wall")} (${hp})`);
|
||||
|
||||
const { press, release } = holdToPeek(() => onpeek?.(tip));
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
onkeydown={() => {}}
|
||||
>
|
||||
<header class="faq-head">
|
||||
<span class="faq-title">{def.name} — Official FAQ</span>
|
||||
<span class="faq-title">Official FAQ on {def.name}</span>
|
||||
<button class="close" onclick={onclose} aria-label="close">×</button>
|
||||
</header>
|
||||
<div class="faq-body">
|
||||
@@ -31,7 +31,7 @@
|
||||
{/each}
|
||||
<p class="colophon">
|
||||
From Tom Jolly's official Wiz-War Rules FAQ (wizwar.com, September 2002).
|
||||
Rulings occasionally reference other editions; the cards overrule the rules,
|
||||
Rulings occasionally reference other editions. The cards overrule the rules,
|
||||
and the designer overrules the table.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
/** Press-and-hold (touch has no hover): report what this fire is. */
|
||||
onpeek?: (tip: string) => void;
|
||||
} = $props();
|
||||
const TIP = "a wall of fire — passing through burns for 4";
|
||||
const TIP = "a wall of fire that burns for 4 as you pass through";
|
||||
const { press, release } = holdToPeek(() => onpeek?.(TIP));
|
||||
const CELL = 48;
|
||||
const WALL = 7;
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
{typeLabel(entryDef)} · {sightOf(entryDef)} · {setLabel(entryDef)}
|
||||
</p>
|
||||
<h3>As printed</h3>
|
||||
<p class="printed">{entryDef.text ?? (entryDef.cardType === "number" ? `A NUMBER card worth ${entryDef.value}: add it to your movement, or set a spell's duration or power.` : "")}</p>
|
||||
<p class="printed">{entryDef.text ?? (entryDef.cardType === "number" ? `A NUMBER card worth ${entryDef.value}. Add it to your movement, or use it to set a spell's duration or power.` : "")}</p>
|
||||
{#if entryDef.faqRulings.length > 0}
|
||||
<h3>Official FAQ</h3>
|
||||
{#each entryDef.faqRulings as q, i (i)}<p>{q}</p>{/each}
|
||||
@@ -198,7 +198,7 @@
|
||||
<span class="booklet-title">Wiz-War</span>
|
||||
<nav class="tabs" aria-label="help sections">
|
||||
<button class:current={tab === "play"} onclick={() => goTab("play")}>How to play</button>
|
||||
<button class:current={tab === "rules"} onclick={() => goTab("rules")}>The rules</button>
|
||||
<button class:current={tab === "rules"} onclick={() => goTab("rules")}>Rules</button>
|
||||
<button class:current={tab === "cards"} onclick={() => goTab("cards")}>Card library</button>
|
||||
<button class:current={tab === "rulings"} onclick={() => goTab("rulings")}>House rulings</button>
|
||||
<button class:current={tab === "about"} onclick={() => goTab("about")}>About</button>
|
||||
@@ -220,7 +220,7 @@
|
||||
</div>
|
||||
<h3>The ledgers in detail</h3>
|
||||
<dl class="tally-list">
|
||||
<dt>{n(stats.gamesCreated)}</dt><dd>games chronicled — {n(stats.gamesStarted)} begun</dd>
|
||||
<dt>{n(stats.gamesCreated)}</dt><dd>games chronicled, {n(stats.gamesStarted)} of them begun</dd>
|
||||
<dt>{n(stats.wizardsSeated)}</dt><dd>distinct wizard names have taken a seat</dd>
|
||||
<dt>{n(stats.commandsPlayed)}</dt><dd>spells, steps, and punches recorded</dd>
|
||||
<dt>{n(stats.longestGameCommands)}</dt><dd>actions in the longest game yet played</dd>
|
||||
@@ -230,9 +230,9 @@
|
||||
</dl>
|
||||
<p class="colophon">
|
||||
{#if stats.firstGameAt}The first game was dealt {new Date(String(stats.firstGameAt)).toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" })}.{/if}
|
||||
Wizards are counted by name, not by person: one player under two names is two wizards here.
|
||||
Wizards are counted by name, so one player under two names counts as two wizards.
|
||||
Table time is estimated from the clock between moves, so a game left open overnight counts only the minutes it was actually played.
|
||||
Games on one device send only their counts — names and moves stay on the device.
|
||||
Games on one device send only their counts, and their names and moves stay on the device.
|
||||
</p>
|
||||
{:else}
|
||||
<p>Counting the ledgers…</p>
|
||||
@@ -243,15 +243,15 @@
|
||||
<h3>Why it exists</h3>
|
||||
<p>
|
||||
In the early nineties, a group of friends played this exact edition
|
||||
for countless hours — the slammed doors, the ambushes around
|
||||
corners, the goat charges, the laughter. That box is long out of
|
||||
for countless hours of slammed doors, ambushes around corners,
|
||||
goat charges and laughter. That box is long out of
|
||||
print, and its cardboard is worn soft at the edges. This digital
|
||||
table was built from that very copy: every card was photographed and
|
||||
transcribed word for word, every board layout verified wall by wall,
|
||||
and the tokens on this screen are the actual hand-inked art from
|
||||
those counter sheets. It is a faithful reproduction of the game as
|
||||
it was played at that table, made so the same friends — and their
|
||||
friends — can keep playing it.
|
||||
it was played at that table, made so the same friends, and their
|
||||
friends, can keep playing it.
|
||||
</p>
|
||||
<h3>What this is</h3>
|
||||
<p>
|
||||
@@ -259,16 +259,16 @@
|
||||
wizards prowl a maze, hurling fireballs, walking through walls,
|
||||
summoning trolls, and stealing each other's treasure. It was created
|
||||
by <strong>Tom Jolly</strong> in 1983 and first published under his
|
||||
own Jolly Games label; the edition reproduced here is the
|
||||
own Jolly Games label. The edition reproduced here is the
|
||||
<strong>sixth edition</strong>, published by Chessex in 1993, together
|
||||
with its one expansion — monsters and magic wands included.
|
||||
with its one expansion, which brings monsters and magic wands.
|
||||
</p>
|
||||
<h3>Whose game it is</h3>
|
||||
<p>
|
||||
Wiz-War is Tom Jolly's design, and the rights to it now rest with
|
||||
<strong>Steve Jackson Games</strong>, who publish the current
|
||||
edition. This project is an unofficial, non-commercial fan work,
|
||||
made with love and admiration for the original — nothing here is
|
||||
made with love and admiration for the original. Nothing here is
|
||||
endorsed by or affiliated with Tom Jolly, Chessex, or Steve Jackson
|
||||
Games. If this game delights you, honor it the right way:
|
||||
<a href="https://www.sjgames.com/wiz-war/" target="_blank" rel="noreferrer">buy
|
||||
@@ -294,21 +294,21 @@
|
||||
<span class="tile-note">the maze through a wizard's own eyes</span>
|
||||
</a>
|
||||
</div>
|
||||
<p class="colophon"><a href="/?fpv&demo=1">Watch the demo</a>: two clockwork wizards play a stretch, then the reel replays it.</p>
|
||||
<p class="colophon"><a href="/?fpv&demo=1">Watch the demo</a>, in which two clockwork wizards play a stretch and the reel replays it.</p>
|
||||
<h3>What the table remembers</h3>
|
||||
<p>
|
||||
An online game is written to this table's ledger move by move, table
|
||||
talk included, and kept: a finished game can be replayed from above
|
||||
talk included, and kept. A finished game can be replayed from above
|
||||
or through a wizard's own eyes, and any turn can be shared by link.
|
||||
A shared replay shows that turn's moves and the sharing wizard's view
|
||||
of the board — never anyone's hand. A game passed around one device
|
||||
stays on that device; only its counts reach the tally. Play under
|
||||
A shared replay shows that turn's moves as an onlooker would have
|
||||
seen them, and never anyone's hand. A game passed around one device
|
||||
stays on that device, and only its counts reach the tally. Play under
|
||||
whatever name you like, and know that what you say and do at an
|
||||
online table is part of its record.
|
||||
</p>
|
||||
<h3>Send word</h3>
|
||||
<p>
|
||||
Feedback, bug reports, and faint praise all welcome — ravens fly to
|
||||
Feedback, bug reports and faint praise are all welcome. Ravens fly to
|
||||
<a href="mailto:eric@ericwagoner.com">eric@ericwagoner.com</a>,
|
||||
<a href="https://bsky.app/profile/kestrelsnest.social" target="_blank" rel="noreferrer">@kestrelsnest.social</a> on Bluesky,
|
||||
or <a href="https://toots.kestrelsnest.social/@eric" target="_blank" rel="noreferrer">@eric@toots.kestrelsnest.social</a> on Mastodon.
|
||||
@@ -324,8 +324,8 @@
|
||||
</ul>
|
||||
{/if}
|
||||
<p class="hosting">
|
||||
The games of Kestrel's Hall are free and will stay free. They run on small servers that cost a few dollars a month; anyone who wants to chip in toward the hosting can at
|
||||
<a href="https://ko-fi.com/kestrelsnest" target="_blank" rel="noreferrer">ko-fi.com/kestrelsnest</a>. Nothing is unlocked by it and nothing is withheld without it.
|
||||
The games of Kestrel's Hall are free and will stay free. They run on small servers that cost a few dollars a month, and anyone who wants to chip in toward the hosting can do so at
|
||||
<a href="https://ko-fi.com/kestrelsnest" target="_blank" rel="noreferrer">ko-fi.com/kestrelsnest</a>. Every game stays whole either way.
|
||||
</p>
|
||||
<p class="colophon">
|
||||
Wiz-War © Tom Jolly. Sixth edition published by Chessex, 1993.
|
||||
@@ -341,10 +341,10 @@
|
||||
{:else if tab === "rulings"}
|
||||
<div class="reading">
|
||||
<p class="colophon">
|
||||
For players who know the box: how this table resolves what the
|
||||
cardboard leaves to the players, by card. If the table rules
|
||||
For players who know the box, here is how this table resolves, card
|
||||
by card, what the cardboard leaves to the players. If the table rules
|
||||
against your memory of the rules, the 🐞 report button pins the
|
||||
moment for review — the reply lands in your lobby.
|
||||
moment for review, and the reply lands in your lobby.
|
||||
</p>
|
||||
<div class="search-row">
|
||||
<input bind:value={rulingsSearch} placeholder="search the rulings by card or topic…" aria-label="search house rulings" />
|
||||
@@ -367,11 +367,12 @@
|
||||
{/each}
|
||||
{#if !rulingsSearch.trim()}
|
||||
<h3 id="ruling-the-table">The table itself</h3>
|
||||
<p>Clockwork wizards — the automatons — play from the same redacted
|
||||
view a human seat gets and play by the rules the engine enforces on
|
||||
everyone; a tier changes what they draw and know.</p>
|
||||
<p>The clockwork wizards, the automatons, play from the same redacted
|
||||
view a human seat gets and by the same rules the engine enforces on
|
||||
everyone. Their tier changes how many cards they draw and how many
|
||||
tricks they know.</p>
|
||||
<p>A turn need not be taken at once. Games wait on the lobby ledger
|
||||
for days; the browser holds your seat; a replay of any game can be
|
||||
for days while the browser holds your seat. A replay of any game can be
|
||||
watched from above or through a wizard's eyes and shared by link.</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -399,7 +400,7 @@
|
||||
{#each refBySource as group (group.source)}
|
||||
<h2 class="divider" id="source-{group.source}">{SOURCE_LABELS[group.source]}</h2>
|
||||
{#if group.source === "summary"}
|
||||
<p class="colophon">Sixth edition rules, condensed from the original rulebook. © 1985 Jolly Games — this digital adaptation is a fan project.</p>
|
||||
<p class="colophon">Sixth edition rules, condensed from the original rulebook. © 1985 Jolly Games. This digital adaptation is a fan project.</p>
|
||||
{:else if group.source === "rulebook"}
|
||||
<p class="colophon">The full text, word for word, for settling table disputes. {RULEBOOK_COPYRIGHT} Notes marked <span class="ed-note">(6E: …)</span> are this table's, on where the sixth edition differs.</p>
|
||||
{:else if group.source === "faq"}
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
</g>
|
||||
{#if onclick}
|
||||
<line {x1} {y1} {x2} {y2} class="illusion-hit"
|
||||
role="button" tabindex="-1" aria-label="a shimmering wall — test your eyes"
|
||||
role="button" tabindex="-1" aria-label="a shimmering wall, to test with your eyes"
|
||||
onclick={(ev) => { ev.stopPropagation(); onclick?.(); }}
|
||||
onkeydown={() => {}}><title>this wall shimmers oddly — tap to test your eyes</title></line>
|
||||
onkeydown={() => {}}><title>this wall shimmers oddly, so tap to test your eyes</title></line>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -708,7 +708,7 @@
|
||||
<div class="replay-scrim">
|
||||
<div class="replay" role="dialog" aria-modal="true" aria-label="what happened while you were away">
|
||||
<header class="replay-head">
|
||||
<span class="replay-title">{moment ? `Instant replay — ${povId}'s turn` : steps[0]?.seq === 0 ? "The whole tale, from the deal" : "While you were away"}</span>
|
||||
<span class="replay-title">{moment ? `Instant replay of ${povId}'s turn` : steps[0]?.seq === 0 ? "The whole tale, from the deal" : "While you were away"}</span>
|
||||
<span class="replay-count">move {idx + 1} of {steps.length}</span>
|
||||
<button class="replay-eyes" class:lit={fp} onclick={() => (fp = !fp)}>
|
||||
{fp ? "⬒ the board" : "👁 your eyes"}</button>
|
||||
@@ -770,7 +770,7 @@
|
||||
<div class="timeline" role="group" aria-label="the moves, a tick each">
|
||||
{#each ticks as t (t.i)}
|
||||
<button class="tick" class:current={t.i === idx} class:turn={t.flag === "turn"} class:hit={t.flag === "hit"} class:gold={t.flag === "gold"} class:end={t.flag === "end"}
|
||||
style:--who={t.color} title={`move ${t.i + 1} — ${t.actor}${t.flag ? ` · ${t.flag === "turn" ? "turn begins" : t.flag === "hit" ? "a blow" : t.flag === "gold" ? "treasure" : "the end"}` : ""}`}
|
||||
style:--who={t.color} title={`move ${t.i + 1}, ${t.actor}${t.flag ? ` · ${t.flag === "turn" ? "turn begins" : t.flag === "hit" ? "a blow" : t.flag === "gold" ? "treasure" : "the end"}` : ""}`}
|
||||
onclick={() => { playing = false; idx = t.i; }} aria-label={`jump to move ${t.i + 1}`}></button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -36,12 +36,12 @@
|
||||
{#if data}
|
||||
<div class="share-title">
|
||||
{#if data.whole}
|
||||
<span class="share-deed">The whole tale{data.actor ? ` — ${data.actor} triumphant` : ""}</span>
|
||||
<span class="share-deed">{data.actor ? `The whole tale of ${data.actor}'s triumph` : "The whole tale"}</span>
|
||||
{:else if data.headline}
|
||||
<span class="share-deed">{data.headline}</span>
|
||||
<span class="share-sub">an instant replay of {data.actor}'s turn{data.round ? `, round ${data.round}` : ""}</span>
|
||||
{:else}
|
||||
<span class="share-deed">Instant replay — {data.actor}'s turn{data.round ? ` (round ${data.round})` : ""}</span>
|
||||
<span class="share-deed">Instant replay of {data.actor}'s turn{data.round ? `, round ${data.round}` : ""}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
{#if failed}
|
||||
<div class="share-note">
|
||||
<p>This replay has wandered off — the link may be old, or the game gone.</p>
|
||||
<p>This replay has wandered off. The link may be old, or the game gone.</p>
|
||||
<a class="share-cta" href="/">▶ deal yourself into a fresh game</a>
|
||||
</div>
|
||||
{:else if !data}
|
||||
@@ -62,7 +62,7 @@
|
||||
{/key}
|
||||
</div>
|
||||
<div class="share-play">
|
||||
<a class="share-cta" href="/">▶ play free — two to six wizards enter the maze</a>
|
||||
<a class="share-cta" href="/">▶ play free, with two to six wizards in the maze</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
<p>
|
||||
<strong>Wiz-War</strong> is Tom Jolly's 1983 game of magical combat in
|
||||
a stone labyrinth. This digital table is an unofficial, non-commercial
|
||||
fan re-creation of the sixth edition (Chessex, 1993) — every card
|
||||
transcribed, every wall verified, replays rebuilt move by move from
|
||||
the game's own ledger.
|
||||
fan re-creation of the sixth edition (Chessex, 1993). Every card is
|
||||
transcribed and every wall verified, and each replay is rebuilt move
|
||||
by move from the game's own ledger.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@ export function summarizeMissed(steps: readonly ReplayStep[], you: string, now:
|
||||
});
|
||||
for (const [who, d] of doings) {
|
||||
const bits = [d.moves > 0 ? `walked ${d.moves} square${d.moves === 1 ? "" : "s"}` : "", d.casts > 0 ? `cast ${d.casts} spell${d.casts === 1 ? "" : "s"}` : ""].filter(Boolean);
|
||||
facts.push({ text: `${who} ${bits.length ? bits.join(" and ") : "passed the time"}; nothing touched you.`, step: d.first });
|
||||
facts.push({ text: bits.length ? `${who} ${bits.join(" and ")}, but nothing touched you.` : `${who} passed the time without troubling you.`, step: d.first });
|
||||
}
|
||||
}
|
||||
if (talk > 0) facts.push({ text: `${talk} line${talk === 1 ? "" : "s"} of table talk.`, step: -1 });
|
||||
|
||||
@@ -144,7 +144,7 @@ export const SCREENPLAYS: Screenplay[] = [
|
||||
...DUEL,
|
||||
name: "warp-sniper",
|
||||
title: "The Warp Sniper",
|
||||
blurb: "Line of sight threads a wormhole: a fireball cast into one mouth arrives through the other.",
|
||||
blurb: "Line of sight threads a wormhole, so a fireball cast into one mouth arrives through the other.",
|
||||
pov: "Wanderer",
|
||||
rig: { Wanderer: ["number-3", "fireball"], Rival: ["number-3"] },
|
||||
beats: [6, 16],
|
||||
@@ -176,7 +176,7 @@ export const SCREENPLAYS: Screenplay[] = [
|
||||
...DUEL,
|
||||
name: "springing-the-trap",
|
||||
title: "Springing the Trap",
|
||||
blurb: "An ambush watches its corridor through a wormhole — and fires the moment a wizard crosses the far sightline.",
|
||||
blurb: "An ambush watches its corridor through a wormhole and fires the moment a wizard crosses the far sightline.",
|
||||
pov: "Rival",
|
||||
rig: { Wanderer: ["opportunity-fire", "fireball"] },
|
||||
beats: [5, 6],
|
||||
@@ -197,7 +197,7 @@ export const SCREENPLAYS: Screenplay[] = [
|
||||
...DUEL,
|
||||
name: "the-leap",
|
||||
title: "The Leap",
|
||||
blurb: "A pit yawns in the corridor — but the die says the intruder sails clean over it, onto the trapper's doorstep.",
|
||||
blurb: "A pit yawns in the corridor, but the die says the intruder sails clean over it, onto the trapper's doorstep.",
|
||||
pov: "Rival",
|
||||
rig: { Wanderer: ["create-pit"] },
|
||||
beats: [8, 9],
|
||||
@@ -221,7 +221,7 @@ export const SCREENPLAYS: Screenplay[] = [
|
||||
...DUEL,
|
||||
name: "come-here",
|
||||
title: "The Puppeteer",
|
||||
blurb: "Mental Force walks the enemy to exactly where you want them — which has its risks.",
|
||||
blurb: "Mental Force walks the enemy to exactly where you want them, and that has its risks.",
|
||||
pov: "Wanderer",
|
||||
rig: { Wanderer: ["mental-force"], Rival: ["number-2"] },
|
||||
beats: [10, 13],
|
||||
@@ -243,7 +243,7 @@ export const SCREENPLAYS: Screenplay[] = [
|
||||
...DUEL,
|
||||
name: "stone-gaze",
|
||||
title: "The Stone Gaze",
|
||||
blurb: "Medusa freezes a wizard solid — and stone, inconveniently, cannot be hurt.",
|
||||
blurb: "Medusa freezes a wizard solid, and stone, inconveniently, cannot be hurt.",
|
||||
pov: "Rival",
|
||||
rig: { Rival: ["number-2", "medusa", "number-5", "fireball"] },
|
||||
beats: [10, 14],
|
||||
@@ -279,7 +279,7 @@ export const SCREENPLAYS: Screenplay[] = [
|
||||
...DUEL,
|
||||
name: "wall-comes-down",
|
||||
title: "Knock Knock",
|
||||
blurb: "Blast the wall between you — the rubble bloodies both sides of it — then step through the breach.",
|
||||
blurb: "Blast the wall between you, bloodying both sides with its rubble, then step through the breach.",
|
||||
pov: "Wanderer",
|
||||
rig: { Wanderer: ["number-2", "destroy-wall"] },
|
||||
beats: [6, 7],
|
||||
@@ -320,7 +320,7 @@ export const SCREENPLAYS: Screenplay[] = [
|
||||
...DUEL,
|
||||
name: "fire-in-the-webs",
|
||||
title: "Fire in the Webs",
|
||||
blurb: "The Sticky Wand binds a wizard in silk — and fire burns twice as hot in a web.",
|
||||
blurb: "The Sticky Wand binds a wizard in silk, and a fireball bites two points harder in a web.",
|
||||
pov: "Rival",
|
||||
rig: { Rival: ["number-2", "sticky-wand", "fireball"] },
|
||||
beats: [11, 14],
|
||||
@@ -340,7 +340,7 @@ export const SCREENPLAYS: Screenplay[] = [
|
||||
...DUEL,
|
||||
name: "dust-and-light",
|
||||
title: "Dust and Light",
|
||||
blurb: "A dust cloud hides her from his spells — so he throws a warp behind her, steps through the light, and she flees blind into her own dust.",
|
||||
blurb: "A dust cloud hides her from his spells, so he throws a warp behind her, steps through the light, and she flees blind into her own dust.",
|
||||
pov: "Rival",
|
||||
rig: { Rival: ["number-2", "dimensional-warp"], Wanderer: ["dust-cloud"] },
|
||||
beats: [12, 14, 20],
|
||||
|
||||
@@ -40,7 +40,7 @@ function describe(view: GameView, cmd: Command): string {
|
||||
case "counteract":
|
||||
return `answer with ${nameOf(view, cmd.instanceId)}`;
|
||||
case "pass":
|
||||
return "let it resolve — nothing in hand is worth spending on this";
|
||||
return "let it resolve, since nothing in hand is worth spending on this";
|
||||
case "punch":
|
||||
return `punch ${cmd.targetId}`;
|
||||
case "punchWall":
|
||||
@@ -56,7 +56,7 @@ function describe(view: GameView, cmd: Command): string {
|
||||
case "warpStep":
|
||||
return "step through the warp underfoot";
|
||||
case "testIllusion":
|
||||
return "test that shimmering wall — doubting it is free";
|
||||
return "test that shimmering wall, since doubting it is free";
|
||||
case "moveCreature":
|
||||
return `march the monster ${DIRECTION[cmd.direction]}`;
|
||||
case "creatureAttack":
|
||||
@@ -90,7 +90,7 @@ export function tableHints(view: GameView): TableHint[] {
|
||||
let advice: string;
|
||||
try {
|
||||
const cmd = automatonCommand(view, s, "archmage");
|
||||
advice = cmd ? `I would ${describe(view, cmd)}.` : "Nothing calls for us right now — wait for your moment.";
|
||||
advice = cmd ? `I would ${describe(view, cmd)}.` : "Nothing calls for us right now. Wait for your moment.";
|
||||
} catch (e) {
|
||||
console.warn("hint council choked:", e);
|
||||
advice = "…I have no counsel here.";
|
||||
|
||||
@@ -77,7 +77,7 @@ class LocalGame {
|
||||
rollTableDie(): void {
|
||||
if (!this.viewerId) return;
|
||||
const roll = 1 + (crypto.getRandomValues(new Uint32Array(1))[0]! % 4);
|
||||
this.log = [...this.log, { text: `\u{1F3B2} ${this.viewerId} rolls the die \u2014 ${roll}`, turn: null, notable: false }];
|
||||
this.log = [...this.log, { text: `\u{1F3B2} ${this.viewerId} rolls a ${roll}.`, turn: null, notable: false }];
|
||||
}
|
||||
|
||||
/** Rebuild the whole game as replay steps (finished games only). */
|
||||
|
||||
@@ -38,7 +38,7 @@ export function humanize(e: GameEvent): string | null {
|
||||
const rolloff = e.players
|
||||
.map((p) => `${p} 🎲 ${(e.dieRolls[p] ?? []).join(", ")}`)
|
||||
.join(" · ");
|
||||
return `Game started. The roll-off: ${rolloff} — ${e.firstPlayer} goes first.`;
|
||||
return `Game started. The roll-off: ${rolloff}. ${e.firstPlayer} goes first.`;
|
||||
}
|
||||
case "turnStarted": return `— ${e.player}'s turn (round ${e.round}) —`;
|
||||
case "turnSkipped": return `${e.player} loses a turn.`;
|
||||
@@ -58,7 +58,7 @@ export function humanize(e: GameEvent): string | null {
|
||||
return `${e.player} counters ${what} with ${cardDef(e.cardId).name}!`;
|
||||
}
|
||||
case "counterNullified": return `${cardDef(e.card.cardId).name} is nullified by Anti-Anti!`;
|
||||
case "attackAbsorbedIntoHand": return `${e.player} absorbs ${cardDef(e.attackCard.cardId).name} into their hand — the card is theirs now!`;
|
||||
case "attackAbsorbedIntoHand": return `${e.player} absorbs ${cardDef(e.attackCard.cardId).name} into their hand, and the card is theirs now!`;
|
||||
case "attackResolved":
|
||||
if (e.redirected) return `The spell is reflected back at ${e.attacker}!`;
|
||||
if (e.fullyStopped) return `The attack is completely stopped.`;
|
||||
@@ -68,42 +68,42 @@ export function humanize(e: GameEvent): string | null {
|
||||
// blow that follows it has nothing to add.
|
||||
if (e.amount === 0 && e.source === "shadow upkeep") return null;
|
||||
const soak = e.soaks?.map((s) => `${s.what} soaks ${s.amount}`).join(", ");
|
||||
return `${e.player} takes ${e.amount} damage (${e.source}${soak ? ` — ${soak}` : ""}) — ${e.lifeAfter} life left.`;
|
||||
return `${e.player} takes ${e.amount} damage (${e.source}${soak ? `, ${soak}` : ""}), ${e.lifeAfter} life left.`;
|
||||
}
|
||||
case "stunned": return `${e.player} is stunned and loses a turn!`;
|
||||
case "knockedBack": return `${e.player} is knocked back ${e.squares} square(s)!`;
|
||||
case "stonesDestroyed": return `${e.player}'s magic stones are destroyed!`;
|
||||
case "wallCreated": return `A wall appears!`;
|
||||
case "wallDestroyed": return e.wasDoor ? `A door is blasted to rubble!` : `A wall crumbles!`;
|
||||
case "warpOpened": return `The outer wall breaches clean through — a new warp opens across the maze!`;
|
||||
case "extraTurnGranted": return `${e.player} speeds up — extra turn banked.`;
|
||||
case "wardWindow": return `The grab hangs in the air — ${e.owner} clutches something…`;
|
||||
case "pushWindow": return `${e.giant} bears down on ${e.pushee} at a fork — which way will they go?`;
|
||||
case "warpOpened": return `The outer wall breaches clean through, and a new warp opens across the maze!`;
|
||||
case "extraTurnGranted": return `${e.player} speeds up and banks an extra turn.`;
|
||||
case "wardWindow": return `The grab hangs in the air as ${e.owner} clutches something…`;
|
||||
case "pushWindow": return `${e.giant} bears down on ${e.pushee} at a fork. Which way will they go?`;
|
||||
case "pushChosen": return `${e.pushee} breaks ${SIDE_NAMES[e.direction].toLowerCase()}.`;
|
||||
case "treasureTorn": return e.toFloor
|
||||
? `${e.attacker} TEARS the treasure from ${e.defender}'s arms — it tumbles to the floor!`
|
||||
? `${e.attacker} TEARS the treasure from ${e.defender}'s arms, and it tumbles to the floor!`
|
||||
: `${e.attacker} TEARS the treasure from ${e.defender}'s arms!`;
|
||||
case "tearResisted": return `${e.defender} clutches the treasure with white knuckles — ${e.attacker} comes away empty!`;
|
||||
case "tearResisted": return `${e.defender} clutches the treasure with white knuckles, and ${e.attacker} comes away empty!`;
|
||||
case "treasureTearAttempted": return `${e.attacker} grabs for the treasure in ${e.defender}'s arms!`;
|
||||
case "powerstoneBoost": return `The powerstone hums over the number already played — ${e.player} gains a stride (${e.newAllowance} total).`;
|
||||
case "powerstoneBoost": return `The powerstone hums over the number already played, and ${e.player} gains a stride (${e.newAllowance} total).`;
|
||||
case "trapSprung": return e.cardId === "gift-from-below"
|
||||
? `${e.player} draws GIFT FROM BELOW — it bites for 3, then deals again!`
|
||||
? `${e.player} draws GIFT FROM BELOW. It bites for 3, then deals again!`
|
||||
: `${e.player} walked into an old TRAP! Lose a turn.`;
|
||||
case "attackMissed": return e.because === "invisible"
|
||||
? `The attack passes through empty air — ${e.defender} is invisible!`
|
||||
: `${e.defender} is too small to hit — the attack misses!`;
|
||||
? `The attack passes through empty air, for ${e.defender} is invisible!`
|
||||
: `${e.defender} is too small to hit, and the attack misses!`;
|
||||
case "damageImmune": return e.because === "bloodstone"
|
||||
? `${e.player}'s bloodstone drinks the whole blow — no damage.`
|
||||
: `${e.player} is stone — the damage has no effect.`;
|
||||
case "lifeGained": return `${e.player} gains ${e.amount} life (${e.source}) — now ${e.lifeAfter}.`;
|
||||
case "madDash": return `${e.player} MAD DASHES — every stride doubles: ${e.newAllowance} movement this turn!`;
|
||||
case "safeOpened": return `${e.player}'s ${cardDef(e.withCardId).name} clicks the safe open — until turn's end.`;
|
||||
case "safeDamaged": return `${e.attacker} batters the safe — ${e.amount} damage (${e.total}/15).`;
|
||||
? `${e.player}'s bloodstone drinks the whole blow, and no damage lands.`
|
||||
: `${e.player} is stone, and the damage has no effect.`;
|
||||
case "lifeGained": return `${e.player} gains ${e.amount} life (${e.source}), now ${e.lifeAfter}.`;
|
||||
case "madDash": return `${e.player} MAD DASHES, every stride doubled, for ${e.newAllowance} movement this turn!`;
|
||||
case "safeOpened": return `${e.player}'s ${cardDef(e.withCardId).name} clicks the safe open until the turn's end.`;
|
||||
case "safeDamaged": return `${e.attacker} batters the safe for ${e.amount} damage (${e.total}/15).`;
|
||||
case "safeSmashed": return `💥 The safe BURSTS open under ${e.attacker}'s assault!`;
|
||||
case "squareContentDamaged": return `${e.attacker} ${e.kind === "ooze" ? "burns" : "batters"} the ${e.kind} — ${e.amount} damage (${e.total}/${e.needed}).`;
|
||||
case "squareContentDamaged": return `${e.attacker} ${e.kind === "ooze" ? "burns" : "batters"} the ${e.kind} for ${e.amount} damage (${e.total}/${e.needed}).`;
|
||||
case "squareContentDestroyed": return e.kind === "ooze" ? `🔥 The ooze burns away under ${e.attacker}'s fire!` : `🌿 The ${e.kind} is torn apart by ${e.attacker}'s attack!`;
|
||||
case "slowDeathWindow": return `Slow Death bites ${e.player} for ${e.points} — a counter hovers over the wound…`;
|
||||
case "slowDeathCountered": return `${e.player}'s ${cardDef(e.cardId).name} blunts the rot — ${e.remaining} point${e.remaining === 1 ? "" : "s"} still coming.`;
|
||||
case "slowDeathWindow": return `Slow Death bites ${e.player} for ${e.points}, and a counter hovers over the wound…`;
|
||||
case "slowDeathCountered": return `${e.player}'s ${cardDef(e.cardId).name} blunts the rot, with ${e.remaining} point${e.remaining === 1 ? "" : "s"} still coming.`;
|
||||
case "spellSustained": return isPermanentDuration(e.turns)
|
||||
? `${spellName(e.cardId)} settles over ${e.target} for good.`
|
||||
: `${spellName(e.cardId)} settles over ${e.target} (${e.turns} turn${e.turns === 1 ? "" : "s"}).`;
|
||||
@@ -112,16 +112,16 @@ export function humanize(e: GameEvent): string | null {
|
||||
? `${e.player} teleports across the maze!`
|
||||
: `${e.player} is teleported away by ${e.by}!`;
|
||||
case "positionsSwapped": return `${e.a} and ${e.b} swap places!`;
|
||||
case "homeBasesSwapped": return `${e.a} and ${e.b} swap home bases — the maze's loyalties shift!`;
|
||||
case "homeBasesSwapped": return `${e.a} and ${e.b} swap home bases, and the maze's loyalties shift!`;
|
||||
case "cardErased": return e.found
|
||||
? `${e.player}'s ${e.cardId ? cardDef(e.cardId).name : "card"} is erased from their mind!`
|
||||
: `${e.player} wasn't holding that card — the erasure fizzles.`;
|
||||
: `${e.player} wasn't holding that card, so the erasure fizzles.`;
|
||||
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 "handRevealed": return `${e.to} reads ${e.player}'s mind, and their hand lies revealed.`;
|
||||
case "cardsDrawnPrivate": return e.cards.length > 0
|
||||
? `You draw ${e.cards.map((c) => cardDef(c.cardId).name).join(", ")}.`
|
||||
: null;
|
||||
case "creationDispelled": return `The ${e.what.replace(/-/g, " ")} unravels — dispelled!`;
|
||||
case "creationDispelled": return `The ${e.what.replace(/-/g, " ")} unravels, dispelled!`;
|
||||
case "firewallCreated": return `A wall of fire roars up (${e.turns} turn${e.turns === 1 ? "" : "s"})!`;
|
||||
case "firewallExpired": return `The wall of fire gutters out.`;
|
||||
case "firewallBurned": return `${e.player} steps through the flames!`;
|
||||
@@ -134,7 +134,7 @@ export function humanize(e: GameEvent): string | null {
|
||||
case "doorReleased": return `The held door swings shut.`;
|
||||
case "doorsRelocked": return `The door swings shut and relocks.`;
|
||||
case "washedBack": return `${e.player} is washed back ${e.blockedSpaces > 0 ? "and crushed against the stone" : "by the wave"}!`;
|
||||
case "enteredThornbush": return `${e.player} pushes into the thorns — and bleeds for it.`;
|
||||
case "enteredThornbush": return `${e.player} pushes into the thorns and bleeds for it.`;
|
||||
case "objectDragged": return `The ${e.what.replace(/-/g, " ")} is dragged across the maze.`;
|
||||
case "cardsDealt": return `${e.player} is dealt ${e.count} card${e.count === 1 ? "" : "s"}.`;
|
||||
case "cardsStolenPrivate": return e.cards.length > 0
|
||||
@@ -142,25 +142,25 @@ export function humanize(e: GameEvent): string | null {
|
||||
: null;
|
||||
case "handRevealedPrivate": return `${e.player}'s hand lies open to you: ${e.cards.map((c) => cardDef(c.cardId).name).join(", ")}.`;
|
||||
case "creatureWarpStepped": return `The creature slips through the dimensional warp!`;
|
||||
case "mentalForceFizzled": return `${e.attacker}'s mental force strains at ${e.defender} — and fails to find a path.`;
|
||||
case "mentalForceFizzled": return `${e.attacker}'s mental force strains at ${e.defender} and fails to find a path.`;
|
||||
case "doorJammed": return `${e.player} jams a door's lock solid.`;
|
||||
case "lockRemoved": return `${e.player} removes a door's lock for good.`;
|
||||
case "cardDisplayed": return `${e.player} displays ${cardDef(e.card.cardId).name}.`;
|
||||
case "lifeTraded": return `${e.player} burns ${e.points} life for speed!`;
|
||||
case "castAroundCorner": return `The spell bends around the corner!`;
|
||||
case "moveBumped": return e.why === "pit"
|
||||
? `${e.player} leaps the pit — nothing to land on beyond it — and teeters back.`
|
||||
? `${e.player} leaps the pit, finds nothing to land on beyond it, and teeters back.`
|
||||
: `${e.player} blunders into a wall!`;
|
||||
case "attackMisdirected": return e.newTarget
|
||||
? `${e.attacker}'s blind attack veers off — and hits ${e.newTarget}!`
|
||||
? `${e.attacker}'s blind attack veers off and hits ${e.newTarget}!`
|
||||
: `${e.attacker}'s blind attack flies off into the darkness.`;
|
||||
case "retreatedInHorror": return `${e.player} flees the hideous sight!`;
|
||||
case "illusionWallCreated": return `A wall appears... or does it?`;
|
||||
case "illusionTested": return e.result === "seesThrough"
|
||||
? `${e.player} sees right through the illusion!`
|
||||
: `${e.player} is convinced the wall is real.`;
|
||||
case "sectorRotated": return `The maze GRINDS — a sector rotates ${e.clockwise ? "clockwise" : "counterclockwise"}!`;
|
||||
case "sectorRelocated": return `The maze SHUDDERS — an entire sector slides away!`;
|
||||
case "sectorRotated": return `The maze GRINDS as a sector rotates ${e.clockwise ? "clockwise" : "counterclockwise"}!`;
|
||||
case "sectorRelocated": return `The maze SHUDDERS as an entire sector slides away!`;
|
||||
case "creatureCreated": return `${e.controller} summons a ${e.kind.replace(/-/g, " ")}!`;
|
||||
case "creatureMoved": return null;
|
||||
case "creatureAttacked":
|
||||
@@ -172,7 +172,7 @@ export function humanize(e: GameEvent): string | null {
|
||||
case "trollRegenerated": return `The troll's stony hide knits itself back together.`;
|
||||
case "shadowUpkeep": return `The shadow drains its master (${e.lifeAfter} life left).`;
|
||||
case "impScorches": return `The fire imp scorches ${e.player}!`;
|
||||
case "monsterBoosted": return `The monster GROWS — its ${e.boost} doubles!`;
|
||||
case "monsterBoosted": return `The monster GROWS, and its ${e.boost} doubles!`;
|
||||
case "wandCharged": return `${e.player} charges a wand (${e.charges} charges).`;
|
||||
case "wandUsed": return e.chargesLeft > 0 ? `The wand crackles (${e.chargesLeft} left).` : null;
|
||||
case "wandExhausted": return `${e.player}'s wand crumbles to dust.`;
|
||||
@@ -190,59 +190,59 @@ export function humanize(e: GameEvent): string | null {
|
||||
case "stuckInSlime": return `${e.player} squelches into the slime and sticks fast.`;
|
||||
case "boobytrapPlaced": return `${e.caster} places four suspicious tokens...`;
|
||||
case "boobytrapSprung": return `SNAP! ${e.player} finds the real boobytrap!`;
|
||||
case "boobytrapBlank": return `${e.player} flips a face-down token — a blank. The rest still wait.`;
|
||||
case "boobytrapBlank": return `${e.player} flips a face-down token and finds a blank. The rest still wait.`;
|
||||
case "objectsGlued": return `Everything on that square is glued down (${e.turns} turns).`;
|
||||
case "safeCreated": return `A massive safe slams down around the loot.`;
|
||||
case "itemsTraded": return `Two items blink and trade places.`;
|
||||
case "stoneTurnedToWater": return `Stone runs like water — a wave crashes out!`;
|
||||
case "stoneTurnedToWater": return `Stone runs like water, and a wave crashes out!`;
|
||||
case "handsSwapped": return `${e.a} and ${e.b} trade entire hands of cards!`;
|
||||
case "handsScrambled": return `CHAOS! Every hand is thrown in a pile and redealt!`;
|
||||
case "rammed": return `BAAA! ${e.attacker} turns into a goat and rams ${e.target} (${e.distance} spaces)!`;
|
||||
case "treasureThrown": return `${e.attacker} HURLS their treasure (${e.distance} spaces)!`;
|
||||
case "illusionBelieved": return e.believed ? `${e.player} flinches — the illusion feels real!` : `${e.player} laughs off the illusion.`;
|
||||
case "illusionBelieved": return e.believed ? `${e.player} flinches, for the illusion feels real!` : `${e.player} laughs off the illusion.`;
|
||||
case "itemStolen": return `${e.to} picks ${e.from}'s pocket.`;
|
||||
case "itemsSwapped": return `${e.a} and ${e.b} swap items.`;
|
||||
case "swapFizzled": return `${e.player}'s trade comes to nothing — the named items were not there to swap.`;
|
||||
case "wardSprung": return `${e.owner}'s treasure was WARDED — it bites ${e.victim}!`;
|
||||
case "swapFizzled": return `${e.player}'s trade comes to nothing, for the named items were not there to swap.`;
|
||||
case "wardSprung": return `${e.owner}'s treasure was WARDED, and it bites ${e.victim}!`;
|
||||
case "curseRemoved": return `${e.caster} lifts a curse from ${e.target}.`;
|
||||
case "objectEnchanted": return `An object gleams with Swarthmore's enchantment.`;
|
||||
case "warpTokensPlaced": return `Two dimensional warp tokens hum to life.`;
|
||||
case "warpStepped": return `${e.player} steps through the dimensional warp!`;
|
||||
case "exitsRedirected": return `The maze's outer exits twist and reconnect!`;
|
||||
case "outOfTurnWindow": return `${e.player} interrupts the flow of time (${e.kind === "interrupt" ? "Interrupt" : "Opportunity Fire"})!`;
|
||||
case "thumbOfGod": return `THE THUMB OF GOD descends! The die crashes down${e.aimedAt.x === e.landedAt.x && e.aimedAt.y === e.landedAt.y ? " dead on target" : " — and drifts"}!`;
|
||||
case "thumbOfGod": return `THE THUMB OF GOD descends! The die crashes down${e.aimedAt.x === e.landedAt.x && e.aimedAt.y === e.landedAt.y ? " dead on target" : ", and drifts"}!`;
|
||||
case "tokenScattered": return `${e.what} goes flying!`;
|
||||
case "ambushSet": return `You commit ${e.spell} to an ambush (${e.via}).`;
|
||||
case "ambushCancelled": return `You quietly disarm your ambush.`;
|
||||
case "ambushSprung": return `AMBUSH! ${e.owner}'s hidden ${e.via.replace(/-/g, " ")} springs on ${e.victim}!`;
|
||||
case "trapRedrawnDuringDeal": return null;
|
||||
case "died": return `☠ ${e.player} is dead${e.killedBy ? ` — killed by ${e.killedBy}` : ""}.`;
|
||||
case "died": return `☠ ${e.player} is dead${e.killedBy ? `, killed by ${e.killedBy}` : ""}.`;
|
||||
case "handTaken": return `${e.to} takes ${e.count} cards from ${e.from}'s body.`;
|
||||
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 "objectPickedUp": return `${e.player} picks up the ${cardDef(e.card.cardId).name}, and their actions are over.`;
|
||||
case "chaosShielded": return `${e.player} raises a FULL SHIELD and sits out the chaos.`;
|
||||
case "tableTalk": return `\u{1F4AC} ${e.player}: ${e.text}`;
|
||||
case "dieRolled": return `\u{1F3B2} ${e.player ?? "The maze"} rolls a ${e.roll} — ${e.purpose}.`;
|
||||
case "dieRolled": return `\u{1F3B2} ${e.player ?? "The maze"} rolls a ${e.roll} (${e.purpose}).`;
|
||||
case "pushed": return e.player
|
||||
? `${e.by} shoves ${e.player} down the corridor!`
|
||||
: `${e.by} shoves the beast down the corridor!`;
|
||||
case "spellTrapped": return `${e.caster} casts ${spellName(e.cardId)} into the slime — it sticks, waiting.`;
|
||||
case "spellTrapped": return `${e.caster} casts ${spellName(e.cardId)} into the slime, where it sticks and waits.`;
|
||||
case "slimeTrapSprung": return `The slime releases its ${spellName(e.cardId)} at ${e.victim}!`;
|
||||
case "slimeWashed": return `The wave washes the slime away.`;
|
||||
case "wallDamaged": {
|
||||
const what = e.needed === 15 ? "door" : "wall";
|
||||
const weapon = e.source === "punch" ? "bare fists" : e.source === "troll" ? "the troll's fist" : cardDef(e.source).name;
|
||||
return `${e.player} batters the ${what} with ${weapon} — ${e.total}/${e.needed}.`;
|
||||
return `${e.player} batters the ${what} with ${weapon} (${e.total}/${e.needed}).`;
|
||||
}
|
||||
case "treasureDropped": return e.onHomeOf ? `${e.player} drops a treasure on ${e.onHomeOf}'s home base!` : `${e.player} drops a treasure.`;
|
||||
case "playerEliminated": return e.reason === "treasuresLost" ? `${e.player} is eliminated — both treasures lost!` : null;
|
||||
case "playerEliminated": return e.reason === "treasuresLost" ? `${e.player} is eliminated, both treasures lost!` : null;
|
||||
case "cardsDiscarded":
|
||||
return `${e.player} discards ${e.cards.map((c) => spellName(c.cardId)).join(", ")}.`;
|
||||
case "cardsDrawn": return e.count > 0 ? `${e.player} draws ${e.count} card(s).` : null;
|
||||
case "deckReshuffled": return `The discard pile is reshuffled (${e.size} cards).`;
|
||||
case "turnEnded": return null;
|
||||
case "gameWon": return `🏆 ${e.player} WINS ${e.reason === "treasures" ? "by treasure!" : "— last wizard standing!"}`;
|
||||
case "gameWon": return `🏆 ${e.player} WINS ${e.reason === "treasures" ? "by treasure!" : "as the last wizard standing!"}`;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -329,7 +329,7 @@ export interface GameSummary {
|
||||
|
||||
export function attentionLabel(a: GameSummary["attention"]): string {
|
||||
switch (a) {
|
||||
case "counteract": return "UNDER ATTACK — respond!";
|
||||
case "counteract": return "UNDER ATTACK! Respond.";
|
||||
case "discard": return "DISCARD to your hand limit";
|
||||
case "interrupt": return "your interruption is waiting";
|
||||
default: return "YOUR TURN";
|
||||
@@ -682,8 +682,8 @@ class Net {
|
||||
this.pendingImage = null;
|
||||
if (image && typeof msg.id === "string") {
|
||||
fetch(`${API_URL}/api/feedback-image/${msg.id}`, { method: "POST", headers: { "content-type": image.type }, body: image })
|
||||
.then((r) => { if (!r.ok) this.flash("The words went through; the picture did not — a PNG or JPEG under 2.5 MB, please"); })
|
||||
.catch(() => this.flash("The words went through; the picture did not"));
|
||||
.then((r) => { if (!r.ok) this.flash("The words went through, but the picture did not. A PNG or JPEG under 2.5 MB, please."); })
|
||||
.catch(() => this.flash("The words went through, but the picture did not."));
|
||||
}
|
||||
this.refreshFeedback();
|
||||
break;
|
||||
@@ -753,7 +753,7 @@ class Net {
|
||||
create(name: string): void {
|
||||
this.you = name;
|
||||
this.spectating = false;
|
||||
if (!this.send({ type: "create", name })) this.flash("Still reaching the table — try again in a moment");
|
||||
if (!this.send({ type: "create", name })) this.flash("Still reaching the table. Try again in a moment.");
|
||||
}
|
||||
|
||||
/** Take a seat in the Peanut Gallery: watch a game with no name and no voice. */
|
||||
@@ -771,7 +771,7 @@ class Net {
|
||||
(s) => s.roomId === this.roomIdPending && s.name === name,
|
||||
);
|
||||
if (!this.send({ type: "join", roomId, name, token: existing?.token ?? this.token })) {
|
||||
this.flash("Still reaching the table — try again in a moment");
|
||||
this.flash("Still reaching the table. Try again in a moment.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,7 +813,7 @@ class Net {
|
||||
/** A word back to the desk under one of your reports, proven by the seat it was filed from. */
|
||||
answerReport(report: { id: string; roomId: string; player?: string }, text: string): boolean {
|
||||
const seat = this.seats.find((s) => s.roomId === report.roomId && (!report.player || s.name === report.player));
|
||||
if (!seat) { this.flash("That seat is no longer in this browser, so the desk cannot be sure it is you"); return false; }
|
||||
if (!seat) { this.flash("That seat is no longer in this browser, so the keeper cannot be sure it is you."); return false; }
|
||||
return this.send({ type: "feedbackAnswer", reportId: report.id, text, seat: $state.snapshot(seat) });
|
||||
}
|
||||
|
||||
@@ -922,8 +922,8 @@ class Net {
|
||||
try {
|
||||
new Notification(
|
||||
g.attention === "counteract"
|
||||
? `Wiz-War — you are under attack in ${g.roomId}!`
|
||||
: `Wiz-War — your turn in ${g.roomId}`,
|
||||
? `Wiz-War · you are under attack in ${g.roomId}!`
|
||||
: `Wiz-War · your turn in ${g.roomId}`,
|
||||
{
|
||||
body: `${g.players.join(" vs ")} · round ${g.round ?? "?"}`,
|
||||
tag: `wizwar-${g.roomId}`,
|
||||
@@ -1055,7 +1055,7 @@ class Net {
|
||||
const label = describeCommand(command);
|
||||
if (!this.send({ type: "command", command })) {
|
||||
this.unsent = label;
|
||||
this.flash(`${label[0]!.toUpperCase()}${label.slice(1)} was not sent — the table is out of reach`);
|
||||
this.flash(`${label[0]!.toUpperCase()}${label.slice(1)} was not sent. The table is out of reach.`);
|
||||
return false;
|
||||
}
|
||||
this.unsent = null;
|
||||
|
||||
@@ -21,8 +21,8 @@ function nameOf(id: string | null): string {
|
||||
export function receiptFor(batch: readonly GameEvent[], e: GameEvent): Receipt | null {
|
||||
if (e.type === "attackMissed") {
|
||||
const why =
|
||||
e.because === "invisible" ? `${e.defender} is invisible; the die sent it astray.`
|
||||
: e.because === "shrink" ? `${e.defender} is shrunk; the die said miss.`
|
||||
e.because === "invisible" ? `${e.defender} is invisible, and the die sent it astray.`
|
||||
: e.because === "shrink" ? `${e.defender} is shrunk, and the die said miss.`
|
||||
: `${e.defender} outran it.`;
|
||||
return { title: `${nameOf(e.attackCardId)} misses ${e.defender}`, lines: [why] };
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export function receiptFor(batch: readonly GameEvent[], e: GameEvent): Receipt |
|
||||
const soak = x.soaks?.map((s) => `${s.what} soaks ${s.amount}`).join(", ");
|
||||
lines.push(`${x.player}: ${x.lifeAfter + x.amount} → ${x.lifeAfter} life${soak ? ` (${soak})` : ""}`);
|
||||
} else if (x.type === "damageImmune") {
|
||||
lines.push(`${x.player} takes nothing — ${x.because}`);
|
||||
lines.push(`${x.player} takes nothing (${x.because})`);
|
||||
} else if (x.type === "lifeGained") {
|
||||
lines.push(`${x.player}: ${x.lifeAfter - x.amount} → ${x.lifeAfter} life, reversed`);
|
||||
} else if (x.type === "stonesDestroyed") {
|
||||
|
||||
@@ -188,11 +188,11 @@ export interface HouseRuling {
|
||||
export const HOUSE_RULINGS: HouseRuling[] = [
|
||||
{
|
||||
id: "thumb-of-god", title: "The Thumb of God", cards: ["thumb-of-god"],
|
||||
body: ["THE THUMB OF GOD is a divine meteor. Aim it at a square; the die drifts up to two squares in a random direction, then every token in and around the landing square — objects, treasures, creatures, even wizards — is flung to a random nearby square. Walls mean nothing to falling cardboard, and there is no counteraction."],
|
||||
body: ["THE THUMB OF GOD is a divine meteor. Aim it at a square and the die drifts up to two squares in a random direction. Then every token in and around the landing square is flung to a random nearby square, whether object, treasure, creature or wizard. Walls mean nothing to falling cardboard, and there is no counteraction."],
|
||||
},
|
||||
{
|
||||
id: "illusion-wall", title: "Illusion walls", cards: ["illusion-wall"],
|
||||
body: ["An ILLUSION WALL is real only to those who believe it. Its creator sees through it from the start; everyone else sees stone until they walk into it or see through it, and the maze remembers each wizard's verdict separately. An untested illusion shimmers faintly, and doubting it is free."],
|
||||
body: ["An ILLUSION WALL is real only to those who believe it. Its creator sees through it from the start. Everyone else sees stone until they walk into it or see through it, and the maze remembers each wizard's verdict separately. An untested illusion shimmers faintly, and doubting it is free."],
|
||||
},
|
||||
{
|
||||
id: "ambush", title: "Ambushes", cards: ["opportunity-fire"],
|
||||
@@ -201,24 +201,24 @@ export const HOUSE_RULINGS: HouseRuling[] = [
|
||||
{
|
||||
id: "butt-head", title: "The goat's ram", cards: ["butt-head", "mad-dash", "power-run"],
|
||||
body: [
|
||||
"BUTT-HEAD's ram is movement. The charge is measured as the shortest walk through the corridors from where you cast it to your victim's square, on the legs you have this turn — three, plus any NUMBER played for movement — and it spends them. You cannot pad the blow by taking the long way round, and the goat lands on the victim's square. There is no ceiling on the damage.",
|
||||
"BUTT-HEAD's ram is movement. The charge is measured as the shortest walk through the corridors from where you cast it to your victim's square, on the legs you have this turn (three, plus any NUMBER played for movement), and it spends them. You cannot pad the blow by taking the long way round, and the goat lands on the victim's square. There is no ceiling on the damage.",
|
||||
"MAD DASH doubles the whole allowance, NUMBER cards and POWER RUN points included, so a well-placed goat can ram for sixteen.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "slime", title: "Spells cast into slime", cards: ["fill-square-with-slime"],
|
||||
body: ["Spells cast at a FILL SQUARE WITH SLIME lodge in the gel, and a slime may hold several. They go off one at a time, oldest first: each wizard who pushes in springs one spell, the next wizard the next. The card says only that each spell goes off once; the queue is this table's reading. A slime shows how many it holds, and a peek names them, since every cast into it was seen."],
|
||||
body: ["Spells cast at a FILL SQUARE WITH SLIME lodge in the gel, and a slime may hold several. They go off one at a time, oldest first, so each wizard who pushes in springs one spell and the next wizard the next. The card says only that each spell goes off once, and the queue is this table's reading. A slime shows how many it holds, and a peek names them, since every cast into it was seen."],
|
||||
},
|
||||
{
|
||||
id: "doors-and-sight", title: "Doors and sight", cards: ["pick-lock", "master-key", "remove-lock"],
|
||||
body: [
|
||||
"A wizard beside a door they can open — its lock removed, unlocked this turn, or PICK LOCK or MASTER KEY in hand — sees through the doorway. The hallway behind them still cannot.",
|
||||
"A wizard beside a door they can open (its lock removed, unlocked this turn, or PICK LOCK or MASTER KEY in hand) sees through the doorway. The hallway behind them still cannot.",
|
||||
"\"The door will relock behind you\" means it: passing through an unlocked door shuts it at the walker's back unless a hand holds it open. A door unlocked and not passed relocks at the turn's end.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "visionstone", title: "Visionstone", cards: ["visionstone", "dust-cloud"],
|
||||
body: ["VISIONSTONE pierces its one wall for every sight the game asks of its bearer — creations and utility spells included, not only direct attacks. It does not see through a DUST CLOUD."],
|
||||
body: ["VISIONSTONE pierces its one wall for every sight the game asks of its bearer, for creations and utility spells as well as direct attacks. It does not see through a DUST CLOUD."],
|
||||
},
|
||||
{
|
||||
id: "dust-cloud", title: "Dust clouds", cards: ["dust-cloud"],
|
||||
@@ -234,11 +234,11 @@ export const HOUSE_RULINGS: HouseRuling[] = [
|
||||
},
|
||||
{
|
||||
id: "disease", title: "Disease", cards: ["disease"],
|
||||
body: ["DISEASE is a plague cast on yourself (\"You're the carrier!\"). The caster carries it; sharing a square bites in both directions; there is no counteraction."],
|
||||
body: ["DISEASE is a plague cast on yourself (\"You're the carrier!\"). The caster carries it, sharing a square bites in both directions, and there is no counteraction."],
|
||||
},
|
||||
{
|
||||
id: "fire-imp", title: "The fire imp's scorch", cards: ["fire-imp"],
|
||||
body: ["The fire imp's scorch is a spell, as the FAQ has it: magical, and counteractable."],
|
||||
body: ["The fire imp's scorch is a spell, as the FAQ has it, so it is magical and can be counteracted."],
|
||||
},
|
||||
{
|
||||
id: "soulstone", title: "Soulstone", cards: ["soulstone"],
|
||||
@@ -246,15 +246,15 @@ export const HOUSE_RULINGS: HouseRuling[] = [
|
||||
},
|
||||
{
|
||||
id: "ward", title: "The ward's bite", cards: ["ward"],
|
||||
body: ["A WARD's bite is counteractable (\"COUNTERACTIONs ... otherwise work as written\"), though nothing a counter does — a reversal, a reflection — touches the ward's caster."],
|
||||
body: ["A WARD's bite is counteractable (\"COUNTERACTIONs ... otherwise work as written\"), though nothing a counter does (a reversal, a reflection) touches the ward's caster."],
|
||||
},
|
||||
{
|
||||
id: "curses", title: "Reversing and reflecting a curse", cards: ["reverse", "full-reflection", "reflection", "slow-death", "walking-dead", "idiot"],
|
||||
body: [
|
||||
"A REVERSE against SLOW DEATH or WALKING DEAD turns the whole curse, as the FAQ rules: a point gained per card drawn, half a point per space walked, permanently.",
|
||||
"A FULL REFLECTION returns a permanent curse — SLOW DEATH, WALKING DEAD, IDIOT — onto its caster instead of letting it evaporate.",
|
||||
"A REVERSE against SLOW DEATH or WALKING DEAD turns the whole curse, as the FAQ rules, into a point gained per card drawn and half a point per space walked, permanently.",
|
||||
"A FULL REFLECTION returns a permanent curse (SLOW DEATH, WALKING DEAD, IDIOT) onto its caster instead of letting it evaporate.",
|
||||
"REFLECTION's returning half is an attack on the caster in its own right, with the caster's own counteraction window: an ABSORB or a BLUNT meets it as it would any blow.",
|
||||
"A REFLECTION against a permanent curse afflicts both wizards. WALKING DEAD and IDIOT say so on the card; SLOW DEATH follows REFLECTION's own rule that a spell works for both parties.",
|
||||
"A REFLECTION against a permanent curse afflicts both wizards. WALKING DEAD and IDIOT say so on the card, and SLOW DEATH follows REFLECTION's own rule that a spell works for both parties.",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -267,25 +267,25 @@ export const HOUSE_RULINGS: HouseRuling[] = [
|
||||
},
|
||||
{
|
||||
id: "teleport", title: "Teleporting across the maze's edge", cards: ["teleport"],
|
||||
body: ["TELEPORT ignores the maze's outer edge as it ignores any wall. A teleporter leaving the maze at any square's edge re-enters at the opposite edge on the same line, one space on — the lettered openings are not needed. Four spaces straight up from two squares below the top edge lands two squares up from the bottom."],
|
||||
body: ["TELEPORT ignores the maze's outer edge as it ignores any wall. A teleporter leaving the maze at any square's edge re-enters at the opposite edge on the same line, one space on, with no need for the lettered openings. Four spaces straight up from the second row lands in the third row from the bottom."],
|
||||
},
|
||||
{
|
||||
id: "power-drain", title: "Power Drain", cards: ["power-drain", "blunt", "absorb"],
|
||||
body: ["POWER DRAIN drains the number played. The caster gains it whether the blow is BLUNTed or ABSORBed — the FAQ has the counter blunting the damage done, not the drain — and a wall drained for its points gives them up as a wizard would. A FULL SHIELD stops the drain with the spell."],
|
||||
body: ["POWER DRAIN drains the number played. The caster gains it whether the blow is BLUNTed or ABSORBed (the FAQ has the counter blunting the damage done and leaving the drain whole), and a wall drained for its points gives them up as a wizard would. A FULL SHIELD stops the drain with the spell."],
|
||||
},
|
||||
{
|
||||
id: "troll", title: "The troll's fist", cards: ["troll"],
|
||||
body: ["A commanded TROLL punches a wall line beside it as it punches a wizard: a D4 of damage toward the wall's fall, once a turn."],
|
||||
body: ["A commanded TROLL punches a wall line beside it as it punches a wizard, for a D4 of damage toward the wall's fall, once a turn."],
|
||||
},
|
||||
{
|
||||
id: "pits", title: "Crossing a pit", cards: ["create-pit"],
|
||||
body: [
|
||||
"Crossing a pit on a 2, 3, or 4 means edging around its rim. The walker lands on an open square beside the pit — the only one if there is one, otherwise the one they name by clicking it. A warp mouth on the rim is a way off like any square, and is taken when no square offers. A pit with no way off cannot be entered.",
|
||||
"Crossing a pit on a 2, 3, or 4 means edging around its rim. The walker lands on an open square beside the pit. If there is more than one, they name it by clicking it. A warp mouth on the rim is a way off like any square, and is taken when no square offers. A pit with no way off cannot be entered.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "big-man", title: "The giant's shove at a fork", cards: ["big-man"],
|
||||
body: ["BIG MAN at a fork: a pushed wizard leaves by any open side but the way the giant came — the only one if there is one, otherwise the side they choose, the giant's stride hanging until they do. The FAQ gives the choice to the other player; so does this table."],
|
||||
body: ["When BIG MAN pushes a wizard to a fork, they leave by any open side but the way the giant came. If more than one is open they choose, and the giant's stride hangs until they do. The FAQ gives the choice to the wizard being pushed, and so does this table."],
|
||||
},
|
||||
{
|
||||
id: "waterwall", title: "The waterwall's wave", cards: ["waterwall"],
|
||||
@@ -293,7 +293,7 @@ export const HOUSE_RULINGS: HouseRuling[] = [
|
||||
},
|
||||
{
|
||||
id: "force-field", title: "Force Field", cards: ["force-field"],
|
||||
body: ["FORCE FIELD, after stopping the spell, stands until the end of the opponent's turn: they may not enter its caster's square, nor cast on or past them, on every side — where the card says one side, this table gives all four."],
|
||||
body: ["FORCE FIELD, after stopping the spell, stands until the end of the opponent's turn. Until then they may not enter its caster's square, or cast on or past them, from any side. The card says one side, and this table gives all four."],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export const RULEBOOK_BASE: RuleSection[] = [
|
||||
title: "EQUIPMENT",
|
||||
paragraphs: [
|
||||
"A four section playing board, a deck of 124 cards, one sheet of cardboard tokens, a rule booklet, and one die (numbered from 1-4). It may be 8-sided or 4-sided, depending on what game edition you have.",
|
||||
"*(6E: 125 cards, two sheets of tokens — see Section 1.)*"
|
||||
"*(6E: 125 cards and two sheets of tokens.)*"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -75,7 +75,7 @@ export const RULEBOOK_BASE: RuleSection[] = [
|
||||
paragraphs: [
|
||||
"There are five different kinds of cards in the game. They are called ATTACK cards, NEUTRAL cards, COUNTER-ACTION cards, NUMBER cards, and MAGIC STONES.",
|
||||
"**ATTACK** cards say \"Attack\" in the upper left-hand corner of each card. **ONLY ONE ATTACK CAN BE USED DURING EACH PLAYER TURN.** In the upper right-hand corner, the abbreviation \"L.O.S.\" might appear. This means that a spell can only be used if a player has a clear line of sight to the person or object the spell is to be cast upon. Line of sight is defined as the line going from the center of the attacker's square to the center of the target's square. The center is the dot marked on each space. In the case of a CREATE WALL spell, the center would be the center of the wall to be created. If this line of sight is interrupted by any part of a wall, the spell cannot be cast.",
|
||||
"*(6E: the boards have no printed dots — eyeball the square centers. See Section 1.)*",
|
||||
"*(6E: the boards have no printed dots, so eyeball the square centers.)*",
|
||||
"**NEUTRAL** cards are non-offensive or non-defensive spells. ANY NUMBER of Neutral spells can be cast during a player's turn. It is not impossible for a player to use his entire hand in one turn.",
|
||||
"**COUNTERACTION** cards are the only cards that can be used OUT OF TURN. They can be used (but aren't usually) during a normal turn as a NEUTRAL card, but their primary use is as a defense against attacks. They are played immediately following an attack by the player who was attacked. You CANNOT counteract a Neutral card. You can, however, use an ABSORB or BLUNT against indirect damage caused to you (e.g., by a falling DESTROYed WALL).",
|
||||
"More than one COUNTERACTION card can be used to defend against a single attack, either used sequentially or added together. It is also possible to COUNTERACT a COUNTERACTION card, so an attack may bounce back and forth a few times before coming to rest. In case a COUNTERACTION card only stops half of a spell, always round the resulting damage UP to the nearest whole number. If all damage from a spell is stopped, any secondary effects, such as a lost turn, are also stopped.",
|
||||
@@ -105,7 +105,7 @@ export const RULEBOOK_BASE: RuleSection[] = [
|
||||
"Damage points, points, damage, and point-based spells all refer to the same thing: things that take away points from you, an opponent, or object (such as a wall or door). They have nothing to do with duration-based spells.",
|
||||
"If a person is eliminated from the game by being killed, then the last attacker gets all the dead person's cards. If a player is eliminated by having lost both his treasures to opponents' home bases, his cards are discarded.",
|
||||
"You cannot physically attack through any type of wall, firewall, or bush. You cannot attack yourself. You cannot attack on the first round of turns, even if you have the SPEED spell.",
|
||||
"*(6E: \"You cannot attack yourself\" is stated in the 6E rulebook; FAQ errata: the first-round restriction should read \"no ATTACKS during the first round\", not \"no combat\" — e.g. Destroy Wall next to someone on turn one is legal.)*",
|
||||
"*(6E: \"You cannot attack yourself\" is stated in the 6E rulebook. The FAQ's errata say the first-round restriction should read \"no ATTACKS during the first round\" rather than \"no combat\", so a Destroy Wall next to someone on turn one is legal.)*",
|
||||
"ANY CARD MAY BE PLAYED WITHOUT A NUMBER CARD, but its power is only \"1\". There are no \"1\" NUMBER cards in the deck.",
|
||||
"When all else fails, you may PUNCH your opponent. This requires no card, but does constitute your attack for the turn. A punch does one point of damage, and you must be in the same square as your opponent to do it. If the object to be hit fills an entire square, you may be in an adjacent square to hit it. It is possible, though time-consuming, to punch a wall down. A wall takes 20 points of damage to destroy; a door takes 15. Any attack against an inanimate object counts as your one attack for the turn. The only inanimate objects you can attack are the walls, doors, and thornbushes.",
|
||||
"It is possible to walk by an opponent or cast a spell past him without affecting him at all, if you want to.",
|
||||
|
||||
@@ -94,14 +94,14 @@ export const HOW_TO_PLAY: RulesSection[] = [
|
||||
{
|
||||
title: "Winning",
|
||||
body: [
|
||||
"Carry two treasures that aren't yours home — one at a time, to your home square, and drop them there — or be the last wizard standing. Lose both of your own treasures to enemy homes and you're out.",
|
||||
"Move and play cards in any order, then end your turn. You have three steps and one attack a turn — a step, a spell, two more steps is a fine turn. The banner above the board keeps count of your moves and your treasures home.",
|
||||
"Carry home two treasures that aren't yours, one at a time, and drop them on your home square. Or be the last wizard standing. If both of your own treasures end up on rival homes, you're out.",
|
||||
"Each turn you have three steps and one attack, to use in any order. You might take a step, cast a spell and take two more steps, then end your turn. The banner above the board keeps count of your moves and the treasures you have carried home.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Moving",
|
||||
body: [
|
||||
"You can move up to three squares on your turn. Click a square next to your wizard to step there; blocked steps are refused with a reason. Standing on a lettered edge opening lights its arrow green and pulses the square it leads to — click the arrow to step through.",
|
||||
"You can move up to three squares on your turn. Click a square next to your wizard to step there, and a blocked step is refused with the reason. When you stand on a lettered opening at the board's edge, its arrow lights green and the square it leads to pulses. Click the arrow to step through.",
|
||||
"To move farther, select a number card and press “Play for +N movement”.",
|
||||
],
|
||||
},
|
||||
@@ -115,23 +115,23 @@ export const HOW_TO_PLAY: RulesSection[] = [
|
||||
{
|
||||
title: "Fighting",
|
||||
body: [
|
||||
"Click an enemy wizard in your square to punch. When you are attacked, the sidebar offers your counteractions — click one to play it, or let the attack resolve.",
|
||||
"Your creatures are the diamond tokens: click one to select it, click adjacent squares to march it, click a wizard or creature in its square to attack.",
|
||||
"Click an enemy wizard in your square to punch. When you are attacked, the sidebar offers your counteractions. Click one to play it, or let the attack resolve.",
|
||||
"Your creatures' tokens are framed in your color. Click one to select it, click the squares beside it to march it, and click a wizard or creature in its square to attack.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Turns, hands, and games",
|
||||
body: [
|
||||
"End your turn with the draw selector set to how many cards you want (up to two). Use “Discard cards…” to throw cards away without playing them.",
|
||||
"Games are saved move-by-move on the server and survive anything. The lobby lists every game you're in and whose turn it is; the tab title and favicon flip when a turn is yours, and “notify me on my turn” adds browser notifications. “Transfer seat” hands a game to another device with a spoken phrase.",
|
||||
"Online games are saved on the server move by move, so a closed tab or a dropped connection loses nothing. The lobby lists every game you're in and whose turn it is. The tab title and icon change when a turn is yours, and “notify me on my turn” adds browser notifications. “Transfer seat” hands a game to another device with a short phrase you enter there.",
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "The automatons",
|
||||
body: [
|
||||
"Any empty seat can be filled with an automaton — a clockwork wizard that plays by the same rules and the same information a human seat gets. Add one in the lobby before the game starts, choosing its temperament: a HUNTER marches for treasure, a BERSERKER hunts wizards over gold, a WORRIER keeps its distance and guards what it has. Pick MYSTERY and the maze deals a temperament in secret — you'll have to read its play to guess its mood, and the roster only unmasks it (🎭) when the game ends.",
|
||||
"The difficulty picker sets how much the clockwork has to work with, never how fairly it plays: an APPRENTICE draws one card a turn and knows a modest spellbook, an ADEPT draws two and defends its gold, and an ARCHMAGE knows every trick in the deck — ambushes, amplifies, walls blasted through, roads denied. Every tier plays its cards correctly; none of them cheats, peeks, or rolls loaded dice.",
|
||||
"The automatons chat a little, hold grudges not at all, and take exactly one action per second so you can watch them think. If one does something that looks brilliant, it probably learned it from a human at this table.",
|
||||
"Any empty seat at an online table can be filled with an automaton, a clockwork wizard that plays by the same rules and sees only what a human seat would see. Add one in the lobby before the game starts and choose its temperament. A HUNTER marches for treasure, a BERSERKER hunts wizards over gold, and a WORRIER keeps its distance and guards what it has. Pick MYSTERY and the maze deals a temperament in secret. You'll have to read its play to guess its mood, and the roster unmasks it (🎭) only when the game ends.",
|
||||
"The difficulty picker sets how much the clockwork has to work with. An APPRENTICE draws one card a turn and knows a modest spellbook, an ADEPT draws two and defends its gold, and an ARCHMAGE knows every trick in the deck: ambushes, amplified spells, walls blasted through, and roads denied. Every tier plays its cards correctly, and none of them cheats, peeks or rolls loaded dice.",
|
||||
"The automatons chat a little, hold no grudges, and take one action a second so you can watch them think. If one does something that looks brilliant, it probably learned it from a human at this table.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user