// Cards as data. A `CardDef` is the course's card format (`course/schema/card.schema.json`), // read straight from JSON. The engine compiles each definition once, when it is // registered, into the lists it reads while resolving: triggers, auras and a // spell's effects. The definitions themselves live in `cards/`, loaded by `content`. /** What a triggered ability listens for. */ export type TriggerOn = | "selfDamaged" // this minion takes damage | "friendlyMinionDamaged" // a minion with the same owner (this one included) takes damage | "anyMinionDamaged" // any minion takes damage | "heroDamaged" // either hero takes damage | "deathrattle" // this minion dies (it resolves after the minion has left play) | "battlecry" // this minion is played from hand | "endOfTurn"; // the end of its owner's turn: the course has no turns, so it never fires /** The trigger's family name, as reported in `Triggered` events. */ export function triggerLabel(on: TriggerOn): string { switch (on) { case "selfDamaged": case "friendlyMinionDamaged": case "anyMinionDamaged": case "heroDamaged": return "damaged"; case "deathrattle": case "battlecry": case "endOfTurn": return on; } } /** * An aura: a continuous effect a minion has while it is in play. The engine reads * auras only at the Aura Update points (see `aura.ts`), never mid-Phase. * * `triggerMultiplier`: triggers listening for `trigger`, belonging to minions with * the aura owner's owner, activate `times` times each. `stacking` says how several * multipliers for the same trigger combine: `"max"` takes the largest (two copies of * a doubler still double); `"additive"` adds each copy's extra activations. */ export type Aura = { type: "triggerMultiplier"; trigger: TriggerOn; times: number; stacking: "max" | "additive" }; /** Who an effect touches. */ export type Target = "self" | "allMinions" | "allOtherMinions" | "allOtherCharacters" | "enemyHero" | "chosen"; /** Which minions an Assemble may pick as its base. An empty filter allows any. */ export interface PoolFilter { /** Only minions with a trigger clause listening for this. */ trigger?: TriggerOn; } /** One effect: a node of a card's ability, interpreted by `effect.ts`. */ export type Effect = | { type: "dealDamage"; target: Target; amount: number } | { type: "draw"; count: number } | { type: "summon"; card: string; count: number } | { type: "gainArmor"; amount: number } | { type: "gainAttack"; target: Target; amount: number } | { type: "destroy"; target: Target } /** Part III (SpellGrid) only: puts a word on the grid. */ | { type: "spawnText"; word: string; at: "self" } /** Get a minion from the pool, with half its text replaced (see `assemble.ts`). */ | { type: "assemble"; filter: PoolFilter } /** Give every minion in the owner's hand +attack/+health. */ | { type: "buffHand"; attack: number; health: number }; /** * One line of card text. A trigger clause applies to the effect clauses after it, * until the next trigger, keyword or aura. Effects before any trigger are what a * spell does when cast. */ export type Clause = | { kind: "keyword"; keyword: string } | { kind: "trigger"; on: TriggerOn } | { kind: "aura"; modifier: Aura } | { kind: "effect"; effect: Effect }; /** A card as data, in the course's card format. */ export interface CardDef { name: string; type: "minion" | "spell"; cost: number; attack?: number; health?: number; noun?: string; text: Clause[]; /** `text.slice(0, split)` is the top half, `text.slice(split)` the bottom half. */ split: number; fictional?: boolean; game?: string; } /** A triggered ability: "whenever , ". */ export interface Trigger { on: TriggerOn; effects: Effect[]; } /** A registered card: its definition, and the definition's text compiled into the lists the engine reads. */ export interface Card { def: CardDef; /** Triggered abilities, in printed order. */ triggers: Trigger[]; /** Auras this minion has while in play. */ auras: Aura[]; /** What the card does when cast (a spell's effects). */ onCast: Effect[]; /** May an Assemble offer this card? Minions with text that were not themselves assembled. */ inPool: boolean; } /** Compile a definition's text. Throws if `split` is past the end of the text. */ export function compile(def: CardDef, inPool: boolean): Card { if (def.split > def.text.length) throw new Error(`"${def.name}": split ${def.split} is past the end of its text`); const card: Card = { def, triggers: [], auras: [], onCast: [], inPool: false }; // The trigger whose effects are still being listed, if any. let open: Trigger | undefined; for (const clause of def.text) { switch (clause.kind) { case "trigger": open = { on: clause.on, effects: [] }; card.triggers.push(open); break; case "effect": (open ? open.effects : card.onCast).push(clause.effect); break; case "aura": card.auras.push(clause.modifier); open = undefined; break; case "keyword": // The course has no combat, so keywords such as Taunt do nothing here. open = undefined; break; } } card.inPool = inPool && def.type === "minion" && def.text.length > 0; return card; } export const hasTrigger = (card: Card, on: TriggerOn): boolean => card.triggers.some((t) => t.on === on); /** Every card the game can create, looked up by name. */ export class CardLibrary { private readonly cards = new Map(); constructor(defs: CardDef[]) { for (const def of defs) this.register(def); } /** Add a card, or replace the card with the same name. It joins the Assemble pool if it is a minion with text. */ register(def: CardDef): void { this.cards.set(def.name, compile(def, true)); } /** Add a card made by Assemble. It never joins the pool. */ registerAssembled(def: CardDef): void { this.cards.set(def.name, compile(def, false)); } has(name: string): boolean { return this.cards.has(name); } get(name: string): Card { const card = this.cards.get(name); if (!card) throw new Error(`unknown card "${name}": add its file to cards/`); return card; } /** The Assemble pool: every card with `inPool`, sorted by name. */ pool(): Card[] { return [...this.cards.values()] .filter((c) => c.inPool) .sort((a, b) => (a.def.name < b.def.name ? -1 : a.def.name > b.def.name ? 1 : 0)); } }