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>
95 lines
2.7 KiB
TypeScript
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;
|
|
}
|