In-game help: how-to-play guide, the 6e rules, and a card library

"Help & rules" in the masthead (lobby and table alike) opens a paper
booklet with three tabs: How To Play (the client's own controls —
moving, casting, attaching numbers and modifiers, fighting, turns,
transfers), The Rules (the 6th edition rules condensed faithfully
from the verified rulebook transcription, exact numbers throughout,
with a Jolly Games colophon), and a Card Library — all 135 playable
cards rendered as the real card faces, searchable by name or text,
each labeled with its quantity and set. Escape closes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 00:06:51 -04:00
co-authored by Claude Fable 5
parent 0221e91ba3
commit 8d10dfee67
3 changed files with 335 additions and 0 deletions
+10
View File
@@ -2,6 +2,7 @@
import { net } from "./net.svelte";
import Board from "./Board.svelte";
import Card from "./Card.svelte";
import Help from "./Help.svelte";
import { allCardDefs, cardDef, isNumberCard, SIDES, stepTarget, cellKey } from "@wizwar/engine";
import type { CardInstance, Side } from "@wizwar/engine";
@@ -11,6 +12,7 @@
let name = $state("");
let joinCode = $state("");
let claimPhrase = $state("");
let showHelp = $state(false);
let drawCount = $state(2);
let withExpansion = $state(true);
/** Your creature selected for movement/attacks. */
@@ -453,11 +455,18 @@
<button class="mast-leave" onclick={() => net.requestTransferCode()}>transfer seat</button>
<button class="mast-leave" onclick={() => net.leave()}>leave table</button>
{/if}
<button class="mast-leave" class:mast-help-solo={!net.roomId} onclick={() => (showHelp = true)}>
help &amp; rules
</button>
<span class="mast-status" class:offline={net.status !== "connected"}>
{net.status === "connected" ? "" : "reconnecting…"}
</span>
</header>
{#if showHelp}
<Help onclose={() => (showHelp = false)} />
{/if}
{#if net.error}
<div class="toast" role="alert">{net.error}</div>
{/if}
@@ -796,6 +805,7 @@
cursor: pointer;
}
.mast-leave:hover { color: #d8d2c0; }
.mast-help-solo { margin-left: auto; }
.toast {
background: #6d2119;
+203
View File
@@ -0,0 +1,203 @@
<script lang="ts">
import { allCardDefs } from "@wizwar/engine";
import Card from "./Card.svelte";
import { HOW_TO_PLAY, RULES_SECTIONS } from "./rules";
let { onclose }: { onclose: () => void } = $props();
let tab = $state<"play" | "rules" | "cards">("play");
let search = $state("");
// The playable pool: every card actually in the 6e game (base + Exp1).
const pool = allCardDefs().filter(
(d) => (d.set === "basic" || d.set === "expansion1") && (d.quantity ?? 0) > 0,
);
const filtered = $derived.by(() => {
const q = search.trim().toLowerCase();
const list = q
? pool.filter(
(d) => d.name.toLowerCase().includes(q) || (d.text ?? "").toLowerCase().includes(q),
)
: pool;
return [...list].sort((a, b) => a.name.localeCompare(b.name));
});
function onkeydown(e: KeyboardEvent) {
if (e.key === "Escape") onclose();
}
</script>
<svelte:window {onkeydown} />
<div class="scrim" role="button" tabindex="-1" onclick={onclose} onkeydown={() => {}}>
<div
class="booklet"
role="dialog"
aria-modal="true"
aria-label="help"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
onkeydown={() => {}}
>
<header class="booklet-head">
<span class="booklet-title">Wiz-War</span>
<nav class="tabs">
<button class:current={tab === "play"} onclick={() => (tab = "play")}>How to play</button>
<button class:current={tab === "rules"} onclick={() => (tab = "rules")}>The rules</button>
<button class:current={tab === "cards"} onclick={() => (tab = "cards")}>Card library</button>
</nav>
<button class="close" onclick={onclose} aria-label="close help">×</button>
</header>
<div class="booklet-body">
{#if tab === "play"}
{#each HOW_TO_PLAY as section (section.title)}
<h3>{section.title}</h3>
{#each section.body as p, i (i)}<p>{p}</p>{/each}
{/each}
{:else if tab === "rules"}
<p class="colophon">
Sixth edition rules, from the original rulebook. © 1985 Jolly Games —
this digital adaptation is a fan project.
</p>
{#each RULES_SECTIONS as section (section.title)}
<h3>{section.title}</h3>
{#each section.body as p, i (i)}<p>{p}</p>{/each}
{/each}
{:else}
<div class="search-row">
<input
bind:value={search}
placeholder="search {pool.length} cards…"
aria-label="search cards"
/>
</div>
<div class="card-grid">
{#each filtered as def (def.id)}
<div class="card-slot">
<Card card={{ instanceId: `lib-${def.id}`, cardId: def.id }} />
<span class="card-meta">
×{def.quantity}{def.alsoIn?.length ? ` (+${def.alsoIn[0]!.quantity} exp)` : ""}
· {def.set === "basic" ? "base" : "exp 1"}
</span>
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>
<style>
.scrim {
position: fixed;
inset: 0;
background: rgba(10, 12, 16, 0.72);
display: grid;
place-items: center;
z-index: 40;
padding: 2rem 1rem;
}
.booklet {
background: #efe8d4;
color: #43331f;
width: min(58rem, 100%);
max-height: calc(100vh - 4rem);
border-radius: 6px;
border: 1px solid #b3a687;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
overflow: hidden;
}
.booklet-head {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.7rem 1rem;
border-bottom: 2px solid #43331f;
flex-wrap: wrap;
}
.booklet-title {
font-family: "Oswald", sans-serif;
font-weight: 700;
font-size: 1.15rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.tabs { display: flex; gap: 0.3rem; }
.tabs button {
font-family: "Oswald", sans-serif;
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
background: none;
border: 1.5px solid transparent;
border-radius: 3px;
color: #6b5a41;
padding: 0.3rem 0.6rem;
cursor: pointer;
}
.tabs button.current {
border-color: #43331f;
color: #43331f;
background: #e4dbc2;
}
.close {
margin-left: auto;
background: none;
border: none;
font-size: 1.4rem;
color: #6b5a41;
cursor: pointer;
line-height: 1;
}
.close:hover { color: #b3372b; }
.booklet-body {
overflow-y: auto;
padding: 1rem 1.4rem 1.4rem;
font-family: "Archivo Narrow", sans-serif;
font-size: 0.98rem;
line-height: 1.5;
}
.booklet-body h3 {
font-family: "Oswald", sans-serif;
font-size: 0.85rem;
letter-spacing: 0.12em;
text-transform: uppercase;
border-bottom: 1px solid #b3a687;
padding-bottom: 0.15rem;
margin: 1.2rem 0 0.4rem;
}
.booklet-body h3:first-child { margin-top: 0; }
.booklet-body p { margin: 0.35rem 0; }
.colophon { font-style: italic; color: #6b5a41; font-size: 0.85rem; }
.search-row { margin-bottom: 0.8rem; }
.search-row input {
width: 100%;
box-sizing: border-box;
background: #f6f0df;
border: 1px solid #b3a687;
border-radius: 4px;
padding: 0.5rem 0.7rem;
font-family: inherit;
font-size: 0.95rem;
color: #43331f;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(8.4rem, 1fr));
gap: 0.8rem;
justify-items: center;
}
.card-slot { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; }
.card-slot :global(.card) { cursor: default; }
.card-slot :global(.card:hover) { transform: none; box-shadow: 0 3px 8px rgba(10, 8, 4, 0.45); }
.card-meta {
font-family: "Courier Prime", monospace;
font-size: 0.68rem;
color: #6b5a41;
}
</style>
+122
View File
@@ -0,0 +1,122 @@
// The 6th edition rules, condensed for play from the verified transcription
// of the owner's physical rulebook (research/official-6e-card-list.md and
// research/rules-6e.md). Wording follows the rulebook; numbers are exact.
export interface RulesSection {
title: string;
body: string[];
}
export const RULES_SECTIONS: RulesSection[] = [
{
title: "Winning",
body: [
"There are two ways to win. One is to eliminate all the other players in wizardly battle. The other is to obtain two treasure chests from any of the other players (they need not both be from the same player), return them one at a time to your home base, and drop them there.",
"You can also LOSE by allowing both of the treasures you protect to be taken to other players' home bases. The moment both of your treasures sit on other players' home bases, you are out of the game.",
],
},
{
title: "Starting out",
body: [
"Each player starts on the home base at the exact center of their sector, with their two treasures on the marked circles nearby. Everyone is dealt seven cards (a TRAP! drawn on the deal is discarded and redrawn). Write 15 life-points for each player — reach 0 and you are dead. There is no upper limit to points you might gain.",
"Turns go clockwise, starting with the player who rolls highest on the die. There is no combat during the first round of turns, and you cannot attack yourself.",
],
},
{
title: "Your turn",
body: [
"You can do a lot during your turn: move, attack once, and use as many NEUTRAL and COUNTERACTION cards as you like. The order doesn't matter, and you can break up your movement while performing other actions.",
"Move up to three spaces per turn. You may add one NUMBER card to your movement (an ADD card lets you join two). The die is never used for movement.",
"If you pick up any object, your turn's actions end the moment you pick it up — you may still draw cards.",
"Draw up to two cards, only at the end of YOUR turn. Hand limit is seven; if you ever exceed it, immediately discard down. You may also discard cards just for the sake of getting new ones.",
],
},
{
title: "The cards",
body: [
"There are five kinds: ATTACK, NEUTRAL, COUNTERACTION, NUMBER, and MAGIC STONE. Corner markings: ADJACENT means the target must be in your square or the one next to you; L.O.S. means you need a clear Line Of Sight from the center of your square to the center of the target's square. Line of sight is blocked by any part of a wall, firewall, or bush.",
"Only one attack can be used during each player turn. Any number of neutrals may be played; casting a neutral on an opponent counts as your attack and can be counteracted.",
"NUMBER cards have three purposes: add to movement, set a spell's DURATION in turns, or set a spell's POWER. Only one number card per action. Any card may be played without a number card — its power or duration is then 1. There are no 1s in the deck.",
"A duration 'turn' starts when the spell is cast and ends at the start of one of the caster's turns.",
],
},
{
title: "Combat",
body: [
"Combat is playing an attack card against an opponent. If they have a COUNTERACTION, they may play it then — counteractions are the only cards playable out of turn, they resolve before attacks, and they can themselves be counteracted, so an attack may bounce back and forth. If a counteraction stops half a spell, round the damage that gets through UP. If ALL damage from a spell is stopped, its secondary effects (like a lost turn) are stopped too.",
"Spells automatically hit unless counteracted. If a spell misses, it dissipates harmlessly.",
"When all else fails, PUNCH: no card needed, one point of damage, same square as your opponent, and it counts as your attack for the turn.",
"A wall takes 20 points of damage to destroy; a door takes 15. Only walls, doors, and thornbushes can be attacked among inanimate things.",
"If you kill an opponent you get all their cards — then immediately discard down to seven. A dead player's home base no longer counts for eliminating others, but their treasures remain usable to win.",
],
},
{
title: "The maze",
body: [
"Walls sit on the lines between spaces and are never diagonal. The apparent double wall where two sectors meet is a single wall.",
"Doors are locked at all times and need special cards to open; after passage a door relocks itself. You can cast a spell through an opened door without stepping through. Only REMOVE LOCK leaves a doorway permanently open; JAM LOCK seals one forever.",
"The lettered openings at the board's edge wrap around: leave at a letter and you re-enter at its matching letter elsewhere on the board (in this game, matching letters are printed at the openings, and standing on one highlights where it leads).",
"You cannot create an object on a home base or an occupied space. If a created wall sits on a sector junction and the sector rotates or relocates, a die roll decides which sector keeps the alteration.",
],
},
{
title: "Objects and treasures",
body: [
"Movable objects include magic stones, treasure chests, the dagger, the large rock, and the wizardblade. Players are not objects (but can be DRAGged). Movable objects don't block line of sight; fixed ones do.",
"You can only carry ONE treasure at a time. Treasures don't count against your hand, and may not be thrown (except with HEAVE-HO). You may retrieve your own treasure, even off another player's home base.",
"Objects can be dropped on your square at any time in your turn, and picked up by whoever lands there later — but picking anything up ends your turn's actions.",
"Magic stones and the Master Key are displayed face-up after first use and remain part of your seven-card hand.",
],
},
{
title: "The die",
body: [
"The die (numbered 1-4) is only for random directions and 50-50 calls. Whenever a random direction is called for, the four faces are the four directions. For a 50-50 chance, 1-2 is one outcome and 3-4 the other.",
],
},
{
title: "Expansion Set #1 — monsters",
body: [
"Monsters follow the commands of their creator (except the IMP), moving during the controlling player's turn. Creating a monster counts as your attack for that turn, and it cannot attack the turn it is created — but it may move.",
"Monsters cannot hurt their creators, carry items, cast spells, or use your cards. You cannot add number cards to their movement, but you can cast neutral spells on them. Monsters can attack other monsters. If a monster kills a player, the controller does NOT get the dead player's cards. If you die, your monsters vanish with you.",
],
},
{
title: "Expansion Set #1 — magic wands",
body: [
"A wand's charges are set by the number card played the first time it is used; each use spends one charge, at most once per turn, and the wand is discarded when empty (it cannot be recharged or retrieved with DEJA-VU).",
"If a wand changes hands, its remaining charges go with it. AMPLIFY and ADD work when setting the initial charges. SPEED and ADRENALINE do not let you use a wand twice in a turn — but the wand's isolation from you means you can use it even under NO SPELL. ABSORB SPELL has no effect on wands.",
],
},
];
export const HOW_TO_PLAY: RulesSection[] = [
{
title: "Moving",
body: [
"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.",
"To move farther, select a number card and press “Play for +N movement”.",
],
},
{
title: "Casting",
body: [
"Click a card to select it, then choose its target: a wizard, a creature, a square, or a wall line, as the hint bar directs. To power a spell, click a number card after selecting the spell, then pick the target. Amplify, Add, Extend, Around The Corner, and Power Attack attach the same way.",
"Untargeted spells (stones, Speed, self-buffs) cast from the Cast button in the hint bar. Nothing ever casts on the first click.",
],
},
{
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.",
],
},
{
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.",
],
},
];