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
+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>