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:
co-authored by
Claude Fable 5
parent
0221e91ba3
commit
8d10dfee67
@@ -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>
|
||||
Reference in New Issue
Block a user