155 lines
4.9 KiB
Rust
155 lines
4.9 KiB
Rust
//! 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<EntityId>,
|
|
amount: i32,
|
|
},
|
|
Draw {
|
|
player: PlayerId,
|
|
},
|
|
GainArmor {
|
|
player: PlayerId,
|
|
amount: i32,
|
|
},
|
|
GainAttack {
|
|
id: EntityId,
|
|
amount: i32,
|
|
},
|
|
Cast {
|
|
card: String,
|
|
player: PlayerId,
|
|
target: Option<EntityId>,
|
|
},
|
|
}
|
|
|
|
/// 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<EntityId>,
|
|
/// The chosen target, if any.
|
|
pub target: Option<EntityId>,
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|