//! The action queue: how one player action (a Sequence) resolves. //! //! Performing an action changes the world and produces events. Every trigger that //! responds to those events goes into one Queue, oldest listener first, and the Queue //! is locked. Each queued trigger then resolves completely, including everything //! its own actions cause, before the next one starts: depth-first. //! //! Module 3: nothing leaves play mid-Sequence. When the outermost Phase ends, the //! Death Creation Step removes the dead, and a Death Phase resolves their //! Deathrattles. That repeats until a step finds no new deaths. //! //! Module 4: a queued trigger may activate more than once. How many times is //! decided by one routine, `activations` in `aura.rs`, from aura data on cards. //! Nothing here knows which cards multiply which triggers. //! //! Module 5: a card's effects are data. Each one is interpreted (`effect.rs`) and //! its actions performed before the next effect of the same ability is read, so //! "Assemble a minion. Give all minions in your hand +2/+2." buffs the new minion. use super::assemble::assemble_from_pool; use super::aura::{activations, aura_update_other}; use super::card::{Effect, PoolFilter, TriggerOn}; use super::death::death_creation_step; use super::effect::actions; use super::event::Event; use super::world::{EntityId, PlayerId, World, Zone}; /// Something the engine can do. Card effects return these instead of changing the world. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Action { /// Deal `amount` to each target at once: all damage first, then the reactions. Damage { targets: Vec, amount: i32, }, Draw { player: PlayerId, }, GainArmor { player: PlayerId, amount: i32, }, GainAttack { id: EntityId, amount: i32, }, Cast { card: String, player: PlayerId, target: Option, }, /// Mark each target pending destroy. They leave play in the next Death Creation Step. Destroy { targets: Vec, }, /// Summon `card` for `player` at board index `at`, or at the right end if `at` is `None`. Summon { card: String, player: PlayerId, at: Option, }, /// Play a minion from its owner's hand: it enters the right end of the board, then its /// Battlecry resolves. Play { id: EntityId, }, /// Put an Assembled minion into `player`'s hand (see `assemble.rs`). Assemble { player: PlayerId, filter: PoolFilter, }, /// Give one entity +attack/+health. Buff { id: EntityId, attack: i32, health: i32, }, } /// What an effect may look at when it runs. pub struct Ctx<'a> { pub world: &'a World, /// The player the effect works for. pub owner: PlayerId, /// The minion whose trigger is resolving; `None` for a spell. pub source: Option, /// The chosen target, if any. pub target: Option, /// The source's board position. For a Deathrattle, the source has left play, /// and this is the position its Death Event remembered. pub position: Option, } impl Ctx<'_> { /// The minion whose trigger is resolving. pub fn minion(&self) -> EntityId { self.source.expect("only a triggered ability has a source") } pub fn enemy_hero(&self) -> EntityId { self.world.hero(1 - self.owner) } } /// Run one Sequence: a player action and everything it causes. pub fn run_sequence(world: &mut World, action: Action) { perform(world, action); // The outermost Phase has ended. Each Death Phase is itself an outermost // Phase, so the end-of-Phase steps run again after it, until nothing dies. let mut deaths = end_of_outermost_phase(world); let mut round = 0; while !deaths.is_empty() { round += 1; world.record(Event::DeathPhase { round }); react(world, &deaths); deaths = end_of_outermost_phase(world); } } /// The steps that run only when the outermost Phase ends; returns the Death Events. /// Hearthstone runs an Aura Update (Health/Attack) first. The course has no /// Health/Attack auras, so that step has nothing to do and is left out. fn end_of_outermost_phase(world: &mut World) -> Vec { let deaths = death_creation_step(world); // After the step: a minion that just died no longer contributes its auras // to the Death Phase that resolves these deaths. aura_update_other(world); deaths } /// Summon `card` for `player` at board index `at` (or the right end). Summoning runs an /// Aura Update. pub fn summon(world: &mut World, card: &str, player: PlayerId, at: Option) -> EntityId { let id = world.create_at(card, player, Zone::Board, at); aura_update_other(world); id } fn perform(world: &mut World, action: Action) { match action { Action::Damage { mut targets, amount, } => { // Area damage: every Damage Event is created, in order of play, before any reaction. targets.sort_by_key(|&id| world.order_of_play(id)); let mut events = Vec::new(); for target in targets { world.damage(target, amount); events.push(Event::Damaged { target, amount }); } react(world, &events); } Action::Draw { player } => world.draw(player), Action::GainArmor { player, amount } => world.gain_armor(player, amount), Action::GainAttack { id, amount } => world.gain_attack(id, amount), Action::Destroy { targets } => { for target in targets { world.mark_destroyed(target); } } Action::Summon { card, player, at } => { summon(world, &card, player, at); } Action::Play { id } => { assert!( world.zone_of(id) == Zone::Hand, "play: entity {id} is not in a hand" ); // Playing is not summoning: no Aura Update here. The played minion's own // auras join at the end of the outermost Phase. world.move_to(id, Zone::Board); let played = Event::Played { id }; world.record(played.clone()); react(world, &[played]); } Action::Cast { card, player, target, } => { let spell = world.card(&card); assert!(spell.is_spell(), "{card:?} is not a spell"); let effects = spell.on_cast.clone(); world.record(Event::SpellCast { card }); run_effects(world, &effects, player, None, target, None); } Action::Assemble { player, filter } => { assemble_from_pool(world, player, &filter); } Action::Buff { id, attack, health } => world.buff(id, attack, health), } } /// Interpret and perform an ability's effects in printed order. Each effect sees the /// world as the effects before it left it. Depth-first: each action, and every /// reaction it causes, finishes before the next. fn run_effects( world: &mut World, effects: &[Effect], owner: PlayerId, source: Option, target: Option, position: Option, ) { for effect in effects { let ctx = Ctx { world, owner, source, target, position, }; for action in actions(effect, &ctx) { perform(world, action); } } } /// A Phase: queue every trigger that responds to `events`, then resolve them one by one. fn react(world: &mut World, events: &[Event]) { // Fill the Queue: for each event (already in order of play), each listener in order of play. let mut queue = Vec::new(); for event in events { for listener in listeners(world, event) { for trigger in &world.card(world.card_name(listener)).triggers { if responds(world, trigger.on, listener, event) { queue.push((listener, trigger.clone(), event)); } } } } // The Queue is now locked: nothing that happens below adds to it. for (source, trigger, event) in queue { // The interceptor: the entry was queued once; the auras in force when it // starts decide how many times it activates, back to back. let times = activations(world, world.owner_of(source), trigger.on); for _ in 0..times { world.record(Event::Triggered { source, on: trigger.on, }); let position = match *event { Event::Destroyed { id, position } if id == source => Some(position), _ => world.board_index(source), }; let owner = world.owner_of(source); run_effects(world, &trigger.effects, owner, Some(source), None, position); } } } /// Who may respond to `event`, in order of play: every minion in play and, for a /// Death Event, the minion that died. It has left play, but its Deathrattle still answers. fn listeners(world: &World, event: &Event) -> Vec { let mut ids = world.minions_in_play(); if let Event::Destroyed { id, .. } = *event { ids.push(id); ids.sort_by_key(|&id| world.order_of_play(id)); } ids } /// Does a trigger listening for `on`, belonging to `listener`, respond to `event`? fn responds(world: &World, on: TriggerOn, listener: EntityId, event: &Event) -> bool { if let Event::Destroyed { id, .. } = *event { return on == TriggerOn::Deathrattle && id == listener; } if let Event::Played { id } = *event { return on == TriggerOn::Battlecry && id == listener; } let Event::Damaged { target, .. } = *event else { return false; }; let is_minion = !world.is_hero(target); match on { TriggerOn::SelfDamaged => target == listener, TriggerOn::FriendlyMinionDamaged => { is_minion && world.owner_of(target) == world.owner_of(listener) } TriggerOn::AnyMinionDamaged => is_minion, TriggerOn::HeroDamaged => !is_minion, TriggerOn::Deathrattle | TriggerOn::Battlecry | TriggerOn::EndOfTurn => false, } }