The designer's rulings, one tap from the card

Tom Jolly's 2002 Rules FAQ settled three table disputes this week
from a text file only I could read. Now it lives in the game: 95
cards gained their FAQ sections (extracted verbatim from the official
document, alias-matched — Slime, Mistbody, Buck and friends resolved
to their proper cards), joining the handful already present for 102
cards with rulings in all. The card faces stay pure card text; an
"Official FAQ" button appears under any enlarged card that has
rulings — the hand inspector, the peek overlay, and as an FAQ chip in
the card library — opening a booklet modal with the rulings and their
provenance: "the cards overrule the rules, and the designer overrules
the table."

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 15:05:27 -04:00
co-authored by Claude Fable 5
parent 6de4254aad
commit bc0c69de11
6 changed files with 314 additions and 16 deletions
+28
View File
@@ -2,6 +2,7 @@
import { attentionLabel, net, spellName } from "./net.svelte";
import Board from "./Board.svelte";
import { PLAYER_COLORS, wizardColor } from "./colors";
import Faq from "./Faq.svelte";
import Card from "./Card.svelte";
import Help from "./Help.svelte";
import Replay from "./Replay.svelte";
@@ -29,6 +30,8 @@
/** Leafing through the face-up discard pile. */
let showDiscards = $state(false);
let chatDraft = $state("");
/** Card whose official FAQ rulings are open. */
let faqCardId = $state<string | null>(null);
let helpTab = $state<"play" | "rules" | "cards" | "about" | "tally">("play");
let hotseatCount = $state(2);
let setupName = $state("");
@@ -768,9 +771,16 @@
<button class="inspector-close" onclick={() => { peekCard = null; peekCreatureId = null; }} aria-label="hide card">×</button>
<div class="inspector-card"><Card card={peekCard} /></div>
{#if peekNote}<div class="peek-note">{peekNote}</div>{/if}
{#if cardDef(peekCard.cardId).faqRulings.length > 0}
<button class="faq-link" onclick={() => (faqCardId = peekCard!.cardId)}>Official FAQ</button>
{/if}
</div>
{/if}
{#if faqCardId}
<Faq cardId={faqCardId} onclose={() => (faqCardId = null)} />
{/if}
{#if showHelp}
<Help initialTab={helpTab} stats={net.stats} onstats={() => net.requestStats()} onclose={() => (showHelp = false)} />
{/if}
@@ -1329,6 +1339,9 @@
<div class="inspector-card">
<Card card={selectedCard} />
</div>
{#if cardDef(selectedCard.cardId).faqRulings.length > 0}
<button class="faq-link" onclick={() => (faqCardId = selectedCard!.cardId)}>Official FAQ</button>
{/if}
</div>
{/if}
@@ -1972,6 +1985,21 @@
}
.discard-empty { font-family: "Caveat", cursive; color: #8a7a5e; font-size: 1.1rem; }
.faq-link {
display: block;
margin: 0.35rem 0 0 auto;
background: #2b2218;
color: #e8dfc6;
border: none;
border-radius: 3px;
font-family: "Oswald", sans-serif;
font-size: 0.62rem;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 0.25rem 0.5rem;
cursor: pointer;
}
.faq-link:hover { background: #43331f; }
.peek-note {
margin-top: 0.4rem;
background: #2b2218;
+95
View File
@@ -0,0 +1,95 @@
<script lang="ts">
import { cardDef } from "@wizwar/engine";
let { cardId, onclose }: { cardId: string; onclose: () => void } = $props();
const def = $derived(cardDef(cardId));
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="faq-book"
role="dialog"
aria-modal="true"
aria-label="official FAQ ruling"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
onkeydown={() => {}}
>
<header class="faq-head">
<span class="faq-title">{def.name} — Official FAQ</span>
<button class="close" onclick={onclose} aria-label="close">×</button>
</header>
<div class="faq-body">
{#each def.faqRulings as ruling, i (i)}
<p>{ruling}</p>
{/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,
and the designer overrules the table.
</p>
</div>
</div>
</div>
<style>
.scrim {
position: fixed;
inset: 0;
background: rgba(10, 12, 16, 0.72);
display: grid;
place-items: center;
z-index: 60;
padding: 1.5rem 1rem;
}
.faq-book {
background: #efe8d4;
color: #43331f;
width: min(34rem, 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;
}
.faq-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 1rem;
border-bottom: 2px solid #43331f;
}
.faq-title {
font-family: "Oswald", sans-serif;
font-weight: 600;
font-size: 0.9rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.close {
background: none;
border: none;
font-size: 1.3rem;
color: #6b5a41;
cursor: pointer;
line-height: 1;
}
.close:hover { color: #b3372b; }
.faq-body {
overflow-y: auto;
padding: 0.9rem 1.2rem 1.1rem;
font-family: "Archivo Narrow", sans-serif;
font-size: 0.95rem;
line-height: 1.5;
}
.faq-body p { margin: 0 0 0.6rem; }
.colophon { font-style: italic; color: #6b5a41; font-size: 0.8rem; }
</style>
+19
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { allCardDefs } from "@wizwar/engine";
import Card from "./Card.svelte";
import Faq from "./Faq.svelte";
import { HOW_TO_PLAY, RULES_SECTIONS } from "./rules";
let {
@@ -17,6 +18,7 @@
// svelte-ignore state_referenced_locally -- the initial tab is intentionally a one-time value
let tab = $state<"play" | "rules" | "cards" | "about" | "tally">(initialTab);
let faqCardId = $state<string | null>(null);
function openTally() {
tab = "tally";
@@ -48,6 +50,10 @@
<svelte:window {onkeydown} />
{#if faqCardId}
<Faq cardId={faqCardId} onclose={() => (faqCardId = null)} />
{/if}
<div class="scrim" role="button" tabindex="-1" onclick={onclose} onkeydown={() => {}}>
<div
class="booklet"
@@ -172,6 +178,9 @@
<span class="card-meta">
×{def.quantity}{def.alsoIn?.length ? ` (+${def.alsoIn[0]!.quantity} exp)` : ""}
· {def.set === "basic" ? "base" : "exp 1"}
{#if def.faqRulings.length > 0}
· <button class="faq-chip" onclick={() => (faqCardId = def.id)}>FAQ</button>
{/if}
</span>
</div>
{/each}
@@ -296,6 +305,16 @@
.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); }
.faq-chip {
background: none;
border: none;
padding: 0;
font: inherit;
color: #8a4a1f;
text-decoration: underline;
text-decoration-style: dotted;
cursor: pointer;
}
.card-meta {
font-family: "Courier Prime", monospace;
font-size: 0.68rem;