Files
wizwar6e/packages/engine/src/cards.ts
T
Eric WagonerandClaude Fable 5 5a0003f4ce Correct 15 cards against photos of the actual 6e card faces
Eric spotted mismatches between the 5e-database-derived card data and
his physical 6e cards (photos IMG_4688-4690). All 15 texts are now
verbatim from the faces. Four were mechanical, now fixed in the
engine: BLIND's 6e text adds "engage in combat" — blinded punches
flail on a die roll; PICK LOCK's face states "This is not a spell" —
the lock cards and thrown weapons are physical actions NO SPELL cannot
silence; MIST-BODY passes doors and drifts through thornbushes but
does NOT pass the maze's stone walls (was backwards); WALL OF FIRE's
counteraction mode stops a Waterbolt (previously deferred). Metadata:
UGLY is L.O.S.-marked, MASTER KEY is NEUTRAL/ADJACENT, WIZARDBLADE
and PICK LOCK carry ADJACENT markings (new `adjacent` field), LARGE
ROCK is rethrowable by any player (already engine behavior),
BRAINSTONE's face has no corner type. 84 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 21:58:17 -04:00

95 lines
2.7 KiB
TypeScript

// Card definitions loaded from data/cards.json (verified against the owner's
// physical 6th edition + Expansion Set #1), and physical-deck construction:
// each printed copy of a card becomes one CardInstance with a stable id.
import cardsData from "../data/cards.json";
export type CardSet = "basic" | "expansion1" | "expansion2";
export type CardType =
| "attack"
| "neutral"
| "counteraction"
| "neutral/counteraction"
| "number"
| "object"
| "trap"
| "artifact"
| "special";
export interface CardDef {
id: string;
name: string;
set: CardSet;
cardType: CardType | null;
subtypes?: string[];
los: boolean | null;
/** Printed ADJACENT corner marking (Pick Lock, Master Key, Wizardblade). */
adjacent?: boolean;
text: string | null;
quantity: number | null;
value?: number; // number cards
alsoIn?: { set: CardSet; quantity: number }[];
faqRulings: string[];
}
export interface CardInstance {
/** e.g. "fireball#2" — stable across the whole game. */
instanceId: string;
cardId: string;
}
const defs: CardDef[] = (cardsData as { cards: CardDef[] }).cards;
const byId = new Map(defs.map((d) => [d.id, d]));
export function cardDef(cardId: string): CardDef {
const def = byId.get(cardId);
if (!def) throw new Error(`unknown card: ${cardId}`);
return def;
}
export function allCardDefs(): readonly CardDef[] {
return defs;
}
/**
* Build the physical deck for the chosen sets. The 6e basic deck is exactly
* 125 cards; adding Expansion Set #1 adds exactly 75 more (including its own
* number cards) — both counts verified against the owner's rulebook lists.
*/
export function buildDeck(sets: CardSet[]): CardInstance[] {
const instances: CardInstance[] = [];
for (const def of defs) {
let copies = 0;
if (sets.includes(def.set) && def.quantity != null) copies += def.quantity;
for (const extra of def.alsoIn ?? []) {
if (sets.includes(extra.set)) copies += extra.quantity;
}
for (let i = 1; i <= copies; i++) {
instances.push({ instanceId: `${def.id}#${i}`, cardId: def.id });
}
}
return instances;
}
export function isNumberCard(cardId: string): boolean {
return cardDef(cardId).cardType === "number";
}
export function numberValue(cardId: string): number {
const def = cardDef(cardId);
if (def.cardType !== "number" || def.value == null) {
throw new Error(`${cardId} is not a number card`);
}
return def.value;
}
/** TRAP! is discarded and redrawn if it comes up during the initial deal. */
export function isTrap(cardId: string): boolean {
return cardId === "trap";
}
/** Magic stones (Powerstone, Shieldstone, ...) — destroyed by Fireball. */
export function isMagicStone(cardId: string): boolean {
return cardDef(cardId).subtypes?.includes("stone") ?? false;
}