Movement planned in full, then committed: a Planner builds a plan (moving and destroyed ids) by running content's movement responses (OPEN/SHUT, PUSH, STOP) for every step, rolling back failed branches. Edge of level blocks; YOU objects move furthest-ahead first; rules are reparsed once after movement. Harness: move/move_dir(Dir), posOf, kindsAt. Expansion Drop: PULL as one word plus one content response. Co-Authored-By: Claude Opus 5.5 <[email protected]>
92 lines
3.3 KiB
Rust
92 lines
3.3 KiB
Rust
//! The interpreter: turns one effect, as data, into engine actions.
|
|
//!
|
|
//! This is the only place that knows what an effect *means*. A card's text is a
|
|
//! small tree (clauses holding effects holding targets); the engine walks it. The
|
|
//! effect vocabulary is closed: a card can only do what this match can do, which is
|
|
//! what makes a data card safe to load.
|
|
|
|
use super::card::{Effect, Target};
|
|
use super::queue::{Action, Ctx};
|
|
use super::world::EntityId;
|
|
|
|
/// The actions one effect performs, in order, given where and for whom it runs.
|
|
pub fn actions(effect: &Effect, ctx: &Ctx) -> Vec<Action> {
|
|
let owner = ctx.owner;
|
|
match effect {
|
|
Effect::DealDamage { target, amount } => vec![Action::Damage {
|
|
targets: targets(*target, ctx),
|
|
amount: *amount,
|
|
}],
|
|
Effect::Draw { count } => (0..*count)
|
|
.map(|_| Action::Draw { player: owner })
|
|
.collect(),
|
|
Effect::Summon { card, count } => (0..*count)
|
|
.map(|_| Action::Summon {
|
|
card: card.clone(),
|
|
player: owner,
|
|
at: summon_position(ctx),
|
|
})
|
|
.collect(),
|
|
Effect::GainArmor { amount } => vec![Action::GainArmor {
|
|
player: owner,
|
|
amount: *amount,
|
|
}],
|
|
Effect::GainAttack { target, amount } => targets(*target, ctx)
|
|
.into_iter()
|
|
.map(|id| Action::GainAttack {
|
|
id,
|
|
amount: *amount,
|
|
})
|
|
.collect(),
|
|
Effect::Destroy { target } => vec![Action::Destroy {
|
|
targets: targets(*target, ctx),
|
|
}],
|
|
Effect::Assemble { filter } => vec![Action::Assemble {
|
|
player: owner,
|
|
filter: filter.clone(),
|
|
}],
|
|
Effect::BuffHand { attack, health } => ctx
|
|
.world
|
|
.minions_in_hand(owner)
|
|
.into_iter()
|
|
.map(|id| Action::Buff {
|
|
id,
|
|
attack: *attack,
|
|
health: *health,
|
|
})
|
|
.collect(),
|
|
Effect::SpawnText { .. } => panic!("spawnText needs a grid: it is a Part III effect"),
|
|
}
|
|
}
|
|
|
|
/// The entities a target names, from where the effect runs.
|
|
fn targets(target: Target, ctx: &Ctx) -> Vec<EntityId> {
|
|
let world = ctx.world;
|
|
let others = |ids: Vec<EntityId>| -> Vec<EntityId> {
|
|
ids.into_iter()
|
|
.filter(|&id| Some(id) != ctx.source)
|
|
.collect()
|
|
};
|
|
match target {
|
|
Target::This => vec![ctx.minion()],
|
|
Target::AllMinions => world.minions_in_play(),
|
|
Target::AllOtherMinions => others(world.minions_in_play()),
|
|
Target::AllOtherCharacters => {
|
|
let mut ids = vec![world.hero(0), world.hero(1)];
|
|
ids.extend(world.minions_in_play());
|
|
others(ids)
|
|
}
|
|
Target::EnemyHero => vec![ctx.enemy_hero()],
|
|
Target::Chosen => ctx.target.into_iter().collect(),
|
|
}
|
|
}
|
|
|
|
/// Where a summon goes: to the right of a source still on the board; where a dead
|
|
/// source stood (its Death Event's position); at the right end for a spell.
|
|
fn summon_position(ctx: &Ctx) -> Option<usize> {
|
|
match ctx.source.and_then(|s| ctx.world.board_index(s)) {
|
|
Some(index) => Some(index + 1),
|
|
None => ctx.position,
|
|
}
|
|
}
|