//! 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. use super::card::TriggerOn; use super::event::Event; use super::world::{EntityId, PlayerId, World}; /// 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, }, } /// 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, } 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); // Module 3: when the outermost Phase ends, deaths are processed here. } 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::Cast { card, player, target, } => { let effect = world .card(&card) .on_cast .unwrap_or_else(|| panic!("{card:?} is not a spell")); world.record(Event::SpellCast { card }); let actions = effect(&Ctx { world, owner: player, source: None, target, }); for action in actions { 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 world.minions_in_play() { for trigger in &world.card(world.card_name(listener)).triggers { if responds(world, trigger.on, listener, event) { queue.push((listener, *trigger)); } } } } // The Queue is now locked: nothing that happens below adds to it. for (source, trigger) in queue { world.record(Event::Triggered { source, on: trigger.on, }); let ctx = Ctx { world, owner: world.owner_of(source), source: Some(source), target: None, }; let actions = (trigger.effect)(&ctx); // Depth-first: each action, and every reaction it causes, finishes before the next. for action in actions { perform(world, action); } } } /// Does a trigger listening for `on`, belonging to `listener`, respond to `event`? fn responds(world: &World, on: TriggerOn, listener: EntityId, event: &Event) -> bool { 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, } }