Scoped to everything since the last pass (695307d). The residue of
fast iteration, removed: a reduced-motion media query that had
swallowed a full copy of the .faq-seal rules; doc comments orphaned
from their functions by inserted methods; the FAQ scrape's seams
(section headings run into ruling bodies under the wrong topics, a
next-page heading shipped as a ruling, an amputated "h", and rulings
filed under alphabetically-nearest strangers — Large Rock/Dagger now
lives on those cards; Book of Spells and Torquemada describe no card
in this set and are gone); the write-only aisle flag left behind by
the reverted rev 7; a linter-silenced dead destructure; a duplicated
median lookup; dead casts; and the fallback path that ignored the
apprentice's one-card draw.
Tests now typecheck (tsconfig includes test/), which surfaced the
missing type imports and a drifted creature literal hiding under an
as-cast. Also: a no-op self-assignment, mid-file imports hoisted, a
dynamic-import habit made static, a hedge comment replaced with a
loud setup failure, the fallback-retries-itself dead rung removed
from both bot drivers, and the twin peek scrims merged.
Deliberately kept: the TIERS lookup guard (ledger JSON is untrusted),
the wand-cost stanzas (they differ on the power-attack trade — a
rules question, not a dedup), and rollDie's coexistence with rollD4
(migrating changes visible logs; future work).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
244 lines
6.7 KiB
Svelte
244 lines
6.7 KiB
Svelte
<script lang="ts">
|
|
import { cardDef } from "@wizwar/engine";
|
|
import type { CardInstance } from "@wizwar/engine";
|
|
|
|
let {
|
|
card,
|
|
selected = false,
|
|
attached = false,
|
|
marked = false,
|
|
displayed = false,
|
|
glowing = false,
|
|
charges = null,
|
|
onclick,
|
|
onfaq,
|
|
}: {
|
|
card: CardInstance;
|
|
selected?: boolean;
|
|
attached?: boolean;
|
|
marked?: boolean;
|
|
displayed?: boolean;
|
|
/** Playable RIGHT NOW out of turn — a golden pulse invites the tap. */
|
|
glowing?: boolean;
|
|
charges?: number | null;
|
|
/** When set, cards with official FAQ rulings wear a tappable FAQ seal. */
|
|
onfaq?: (cardId: string) => void;
|
|
onclick?: () => void;
|
|
} = $props();
|
|
|
|
const def = $derived(cardDef(card.cardId));
|
|
const isNumber = $derived(def.cardType === "number");
|
|
|
|
const leftRail = $derived.by(() => {
|
|
switch (def.cardType) {
|
|
case "attack": return "ATTACK";
|
|
case "neutral": return "NEUTRAL";
|
|
case "counteraction": return "COUNTERACTION";
|
|
case "neutral/counteraction": return "NEUTRAL/COUNTERACTION";
|
|
case "trap": return "TRAP";
|
|
default: return "";
|
|
}
|
|
});
|
|
const rightRail = $derived(def.los ? "L.O.S." : def.adjacent ? "ADJACENT" : "");
|
|
// The text box is fixed; the type shrinks until every word fits. Longest
|
|
// card in the set is ~550 chars (Firefly Stick).
|
|
const textSize = $derived.by(() => {
|
|
const n = def.text?.length ?? 0;
|
|
if (n <= 130) return "0.6rem";
|
|
if (n <= 200) return "0.56rem";
|
|
if (n <= 280) return "0.5rem";
|
|
if (n <= 370) return "0.45rem";
|
|
if (n <= 470) return "0.41rem";
|
|
return "0.375rem";
|
|
});
|
|
const textLeading = $derived((def.text?.length ?? 0) > 280 ? "1.18" : "1.28");
|
|
</script>
|
|
|
|
<button
|
|
class="card"
|
|
class:selected
|
|
class:attached
|
|
class:glowing
|
|
class:marked
|
|
class:number={isNumber}
|
|
{onclick}
|
|
title={def.text ?? def.name}
|
|
>
|
|
{#if isNumber}
|
|
<span class="corner tl">{def.value}</span>
|
|
<span class="numeral" data-value={def.value}>{def.value}</span>
|
|
<span class="brand">Wiz-War</span>
|
|
<span class="corner br">{def.value}</span>
|
|
{:else}
|
|
{#if leftRail}<span class="rail left">{leftRail}</span>{/if}
|
|
{#if rightRail}<span class="rail right">{rightRail}</span>{/if}
|
|
<span class="name">{def.name}</span>
|
|
{#if def.text}<span class="text" style:font-size={textSize} style:line-height={textLeading}>{def.text}</span>{/if}
|
|
{/if}
|
|
{#if displayed}
|
|
<span class="badge">DISPLAYED{charges != null ? ` · ${charges}` : ""}</span>
|
|
{/if}
|
|
{#if onfaq && def.faqRulings.length > 0}
|
|
<span
|
|
class="faq-seal" role="button" tabindex="-1"
|
|
onclick={(ev) => { ev.stopPropagation(); onfaq?.(card.cardId); }}
|
|
onkeydown={() => {}}
|
|
>FAQ</span>
|
|
{/if}
|
|
</button>
|
|
|
|
<style>
|
|
.card {
|
|
position: relative;
|
|
width: 8.2rem;
|
|
height: 11.4rem;
|
|
flex: 0 0 auto;
|
|
background:
|
|
radial-gradient(ellipse at 30% 20%, rgba(255, 252, 240, 0.5), transparent 60%),
|
|
#e9e1cb;
|
|
border: 1px solid #b3a687;
|
|
border-radius: 7px;
|
|
box-shadow:
|
|
0 1px 0 rgba(255, 255, 255, 0.5) inset,
|
|
0 3px 8px rgba(10, 8, 4, 0.45);
|
|
color: #5f4a33;
|
|
padding: 0.55rem 1.05rem 0.5rem;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
overflow: hidden;
|
|
transition: transform 120ms ease, box-shadow 120ms ease;
|
|
}
|
|
.card:hover {
|
|
transform: translateY(-0.55rem);
|
|
box-shadow: 0 8px 18px rgba(10, 8, 4, 0.55);
|
|
z-index: 3;
|
|
}
|
|
.card:focus-visible {
|
|
outline: 2px solid #d8b23a;
|
|
outline-offset: 2px;
|
|
}
|
|
.card.selected {
|
|
transform: translateY(-0.8rem);
|
|
box-shadow:
|
|
0 0 0 2.5px #d8b23a,
|
|
0 10px 20px rgba(10, 8, 4, 0.6);
|
|
}
|
|
.card.attached {
|
|
transform: translateY(-0.5rem);
|
|
box-shadow: 0 0 0 2.5px #2e7d32, 0 8px 16px rgba(10, 8, 4, 0.55);
|
|
}
|
|
.card.marked {
|
|
box-shadow: 0 0 0 2.5px #b3372b, 0 4px 10px rgba(10, 8, 4, 0.5);
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.rail {
|
|
position: absolute;
|
|
top: 0.55rem;
|
|
font-family: "Oswald", sans-serif;
|
|
font-size: 0.42rem;
|
|
font-weight: 500;
|
|
letter-spacing: 0.14em;
|
|
color: #4a3a26;
|
|
writing-mode: vertical-lr;
|
|
text-orientation: upright;
|
|
line-height: 1;
|
|
max-height: 9.5rem;
|
|
overflow: hidden;
|
|
}
|
|
.rail.left { left: 0.28rem; }
|
|
.rail.right { right: 0.28rem; }
|
|
|
|
.name {
|
|
font-family: "Oswald", sans-serif;
|
|
font-weight: 600;
|
|
font-size: 0.78rem;
|
|
line-height: 1.05;
|
|
letter-spacing: 0.02em;
|
|
text-transform: uppercase;
|
|
color: #43331f;
|
|
margin: 0.35rem 0 0.3rem;
|
|
}
|
|
.text {
|
|
font-family: "Archivo Narrow", sans-serif;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Number cards: a big numeral in a bordered plate, like the originals. */
|
|
.numeral {
|
|
font-family: "Oswald", sans-serif;
|
|
font-weight: 700;
|
|
font-size: 3.4rem;
|
|
line-height: 1;
|
|
color: #43331f;
|
|
margin-top: 1.6rem;
|
|
padding: 0.4rem 1rem 0.5rem;
|
|
border: 2px solid #5f4a33;
|
|
}
|
|
.numeral[data-value="3"] { border-style: dashed; }
|
|
.numeral[data-value="4"] { border-radius: 12px; }
|
|
.numeral[data-value="5"] { border-style: double; border-width: 4px; }
|
|
.numeral[data-value="6"] { border-style: none; outline: 2px dotted #5f4a33; outline-offset: 4px; }
|
|
.brand {
|
|
font-family: "Oswald", sans-serif;
|
|
font-size: 0.6rem;
|
|
letter-spacing: 0.06em;
|
|
margin-top: 0.45rem;
|
|
}
|
|
.corner {
|
|
position: absolute;
|
|
font-family: "Oswald", sans-serif;
|
|
font-weight: 600;
|
|
font-size: 0.75rem;
|
|
}
|
|
.corner.tl { top: 0.3rem; left: 0.45rem; }
|
|
.corner.br { bottom: 0.3rem; right: 0.45rem; transform: rotate(180deg); }
|
|
|
|
.badge {
|
|
position: absolute;
|
|
bottom: 0.3rem;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #43331f;
|
|
color: #e9e1cb;
|
|
font-family: "Oswald", sans-serif;
|
|
font-size: 0.45rem;
|
|
letter-spacing: 0.12em;
|
|
padding: 0.12rem 0.4rem;
|
|
border-radius: 2px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.faq-seal {
|
|
position: absolute;
|
|
bottom: 0.15rem;
|
|
right: 0.2rem;
|
|
background: #2b2218;
|
|
color: #e8dfc6;
|
|
font-family: "Oswald", sans-serif;
|
|
font-size: 0.42rem;
|
|
letter-spacing: 0.12em;
|
|
padding: 0.1rem 0.25rem;
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
opacity: 0.85;
|
|
}
|
|
.faq-seal:hover { opacity: 1; }
|
|
.card.glowing {
|
|
box-shadow: 0 0 0 2px #c9a72a, 0 0 14px 3px rgba(201, 167, 42, 0.55);
|
|
animation: card-glow 1.4s ease-in-out infinite;
|
|
}
|
|
@keyframes card-glow {
|
|
0%, 100% { box-shadow: 0 0 0 2px #c9a72a, 0 0 14px 3px rgba(201, 167, 42, 0.55); }
|
|
50% { box-shadow: 0 0 0 2px #c9a72a, 0 0 6px 1px rgba(201, 167, 42, 0.3); }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.card.glowing { animation: none; }
|
|
.card { transition: none; }
|
|
.card:hover, .card.selected, .card.attached { transform: none; }
|
|
}
|
|
</style>
|