//! 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`. use std::collections::HashMap; use serde::Deserialize; /// What a triggered ability listens for. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] pub enum TriggerOn { /// This minion takes damage. SelfDamaged, /// A minion with the same owner (this one included) takes damage. FriendlyMinionDamaged, /// Any minion takes damage. AnyMinionDamaged, /// Either hero takes damage. HeroDamaged, /// This minion dies. It resolves after the minion has left play. Deathrattle, /// This minion is played from hand. Battlecry, /// The end of its owner's turn. The course has no turns, so it never fires; card /// data may still name it, for example in a trigger multiplier. EndOfTurn, } impl TriggerOn { /// The trigger's family name, as reported in `Triggered` events. pub fn label(self) -> &'static str { match self { TriggerOn::SelfDamaged | TriggerOn::FriendlyMinionDamaged | TriggerOn::AnyMinionDamaged | TriggerOn::HeroDamaged => "damaged", TriggerOn::Deathrattle => "deathrattle", TriggerOn::Battlecry => "battlecry", TriggerOn::EndOfTurn => "endOfTurn", } } } /// How several trigger multipliers for the same trigger combine. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Stacking { /// The largest multiplier wins: two copies of a doubler still double. Max, /// Each copy adds its extra activations. Additive, } /// 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.rs`), never mid-Phase. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Aura { /// Triggers listening for `trigger`, belonging to minions with the aura owner's /// owner, activate `times` times each. TriggerMultiplier { trigger: TriggerOn, times: u32, stacking: Stacking, }, /// Damage that would be dealt to the aura owner's hero is dealt to the aura's /// minion instead (see `DAMAGE_STEPS` in `queue.rs`). DamageRedirect { from: RedirectFrom, to: RedirectTo }, } /// Whose damage a redirect takes: the card format's `"friendlyHero"`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] pub enum RedirectFrom { FriendlyHero, } /// Who takes redirected damage. `This` is the card format's `"self"`: the aura's minion. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] pub enum RedirectTo { #[serde(rename = "self")] This, } /// Who an effect touches. `This` is the schema's `"self"`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Target { #[serde(rename = "self")] This, AllMinions, AllOtherMinions, AllOtherCharacters, EnemyHero, Chosen, } /// Which minions an Assemble may pick as its base. An empty filter allows any. #[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] pub struct PoolFilter { /// Only minions with a trigger clause listening for this. #[serde(default)] pub trigger: Option, } /// One effect: a node of a card's ability, interpreted by `effect.rs`. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Effect { DealDamage { target: Target, amount: i32, }, Draw { count: u32, }, Summon { card: String, count: u32, }, GainArmor { amount: i32, }, GainAttack { target: Target, amount: i32, }, Destroy { target: Target, }, /// Part III (SpellGrid) only: puts a word on the grid. SpawnText { word: String, at: String, }, /// Get a minion from the pool, with half its text replaced (see `assemble.rs`). Assemble { filter: PoolFilter, }, /// Give every minion in the owner's hand +attack/+health. BuffHand { attack: i32, health: i32, }, } /// 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. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum Clause { Keyword { keyword: String }, Trigger { on: TriggerOn }, Aura { modifier: Aura }, Effect { effect: Effect }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] pub enum CardType { Minion, Spell, } /// A card as data, in the course's card format. Fields the engine doesn't use /// (`fictional`, `game`, `noun`) are ignored. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct CardDef { pub name: String, #[serde(rename = "type")] pub card_type: CardType, pub cost: i32, #[serde(default)] pub attack: i32, #[serde(default)] pub health: i32, pub text: Vec, /// `text[..split]` is the top half, `text[split..]` the bottom half. pub split: usize, } /// A triggered ability: "whenever , ". #[derive(Debug, Clone, PartialEq, Eq)] pub struct Trigger { pub on: TriggerOn, pub effects: Vec, } /// A registered card: its definition, and the definition's text compiled into the /// lists the engine reads while resolving. #[derive(Debug, Clone)] pub struct Card { pub def: CardDef, /// Triggered abilities, in printed order. pub triggers: Vec, /// Auras this minion has while in play. pub auras: Vec, /// What the card does when cast (a spell's effects). pub on_cast: Vec, /// May an Assemble offer this card? Minions with text that were not themselves assembled. pub in_pool: bool, } impl Card { /// Compile a definition's text. Panics if `split` is past the end of the text. pub fn compile(def: CardDef, in_pool: bool) -> Card { assert!( def.split <= def.text.len(), "{:?}: split {} is past the end of its text", def.name, def.split ); let mut triggers: Vec = Vec::new(); let mut auras = Vec::new(); let mut on_cast = Vec::new(); // Is the latest clause a trigger whose effects are still being listed? let mut open_trigger = false; for clause in &def.text { match clause { Clause::Trigger { on } => { triggers.push(Trigger { on: *on, effects: Vec::new(), }); open_trigger = true; } Clause::Effect { effect } if open_trigger => { triggers.last_mut().unwrap().effects.push(effect.clone()); } Clause::Effect { effect } => on_cast.push(effect.clone()), Clause::Aura { modifier } => { auras.push(*modifier); open_trigger = false; } // The course has no combat, so keywords such as Taunt do nothing here. Clause::Keyword { .. } => open_trigger = false, } } let in_pool = in_pool && def.card_type == CardType::Minion && !def.text.is_empty(); Card { def, triggers, auras, on_cast, in_pool, } } pub fn is_spell(&self) -> bool { self.def.card_type == CardType::Spell } pub fn has_trigger(&self, on: TriggerOn) -> bool { self.triggers.iter().any(|t| t.on == on) } } /// Does `text` mean what it says? For a minion, every effect clause belongs to a /// trigger (none comes before the first trigger, or after an aura or keyword), and /// every trigger has at least one effect before the next trigger, aura, keyword or /// the end. The rule is for minions (Assemble makes only minions): a spell passes. pub fn well_formed(text: &[Clause], card_type: CardType) -> bool { if card_type != CardType::Minion { return true; } // The trigger whose effects are being listed: none, one with no effect yet, or one with some. #[derive(PartialEq)] enum Open { None, Empty, Filled, } let mut open = Open::None; for clause in text { match clause { Clause::Effect { .. } if open == Open::None => return false, Clause::Effect { .. } => open = Open::Filled, _ if open == Open::Empty => return false, Clause::Trigger { .. } => open = Open::Empty, Clause::Aura { .. } | Clause::Keyword { .. } => open = Open::None, } } open != Open::Empty } /// Every card the game can create, looked up by name. #[derive(Debug, Default)] pub struct CardLibrary { cards: HashMap, } impl CardLibrary { pub fn new(defs: Vec) -> CardLibrary { let mut library = CardLibrary::default(); for def in defs { library.register(def); } library } /// Add a card, or replace the card with the same name. It joins the Assemble pool /// if it is a minion with text. pub fn register(&mut self, def: CardDef) { self.insert(Card::compile(def, true)); } /// Add a card made by Assemble. It never joins the pool. pub fn register_assembled(&mut self, def: CardDef) { self.insert(Card::compile(def, false)); } fn insert(&mut self, card: Card) { self.cards.insert(card.def.name.clone(), card); } pub fn contains(&self, name: &str) -> bool { self.cards.contains_key(name) } pub fn get(&self, name: &str) -> &Card { self.cards .get(name) .unwrap_or_else(|| panic!("unknown card {name:?}: add its file to cards/")) } /// The Assemble pool: every card with `in_pool`, sorted by name. pub fn pool(&self) -> Vec<&Card> { let mut pool: Vec<&Card> = self.cards.values().filter(|c| c.in_pool).collect(); pool.sort_by(|a, b| a.def.name.cmp(&b.def.name)); pool } }