Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions crates/engine/src/analysis/ability_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1166,8 +1166,7 @@ fn trigger_axis(trig: &TriggerDefinition) -> Option<AxisKey> {
| TriggerMode::DungeonCompleted
| TriggerMode::RoomEntered
| TriggerMode::PlanarDice
| TriggerMode::PlaneswalkedFrom
| TriggerMode::PlaneswalkedTo
| TriggerMode::Planeswalked { .. }
| TriggerMode::ChaosEnsues
| TriggerMode::RolledDie
| TriggerMode::RolledDieOnce
Expand Down
5 changes: 5 additions & 0 deletions crates/engine/src/game/ability_rw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1975,6 +1975,7 @@ fn legacy_static_condition(x: &StaticCondition) -> bool {
| StaticCondition::ControlsCommander { .. }
| StaticCondition::SourceControllerEquals { .. }
| StaticCondition::EnchantedIsFaceDown
| StaticCondition::SourceIsFaceUp
| StaticCondition::AdditionalCostPaid
| StaticCondition::CastingAsVariant { .. }
| StaticCondition::None => false,
Expand Down Expand Up @@ -6047,6 +6048,10 @@ fn rw_static_condition(x: &StaticCondition) -> RwProfile {
| StaticCondition::SourceAttachedToCreature
| StaticCondition::SourceIsPaired
| StaticCondition::SourceInZone { .. }
// CR 311.2: the source plane's face-up status is a source-object status
// read (command-zone membership), grouped with the other frozen source
// status flags (saddled/monstrous/…).
| StaticCondition::SourceIsFaceUp
| StaticCondition::WasStartingPlayer { .. } => frozen_source_read(),
StaticCondition::RecipientHasCounters { .. }
| StaticCondition::RecipientMatchesFilter { .. }
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/game/ability_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3024,6 +3024,7 @@ fn scan_static_condition(x: &StaticCondition) -> Axes {
StaticCondition::SourceIsPaired => Axes::NONE,
StaticCondition::SourceInZone { zone: _ } => Axes::NONE,
StaticCondition::EnchantedIsFaceDown => Axes::NONE,
StaticCondition::SourceIsFaceUp => Axes::NONE,
StaticCondition::AdditionalCostPaid => Axes::NONE,
StaticCondition::CastingAsVariant { variant: _ } => Axes::NONE,
StaticCondition::None => Axes::NONE,
Expand Down
4 changes: 4 additions & 0 deletions crates/engine/src/game/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4043,6 +4043,7 @@ fn fmt_static_condition(cond: &StaticCondition) -> String {
SC::SourceIsPaired => "source is paired".into(),
SC::SourceInZone { zone } => format!("source is in {}", fmt_zone(zone)),
SC::EnchantedIsFaceDown => "enchanted creature is face-down".into(),
SC::SourceIsFaceUp => "source plane is face up".into(),
SC::AdditionalCostPaid => "additional cost was paid".into(),
SC::CastingAsVariant { variant } => format!("casting as {variant:?}"),
SC::None => "none".into(),
Expand Down Expand Up @@ -7453,6 +7454,9 @@ fn static_condition_feature(cond: &StaticCondition) -> (&'static str, FeatureSup
// object's zone against the specified zone. Runtime-handled.
StaticCondition::SourceInZone { .. } => ("SourceInZone", Handled),
StaticCondition::EnchantedIsFaceDown => ("EnchantedIsFaceDown", Handled),
// CR 311.2 / CR 901.7: evaluated by `layers::evaluate_condition` against
// the command-zone active plane. Runtime-handled.
StaticCondition::SourceIsFaceUp => ("SourceIsFaceUp", Handled),
StaticCondition::AdditionalCostPaid => ("AdditionalCostPaid", Handled),
StaticCondition::CastingAsVariant { .. } => ("CastingAsVariant", Handled),
}
Expand Down
60 changes: 59 additions & 1 deletion crates/engine/src/game/effects/delayed_trigger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,7 @@ mod tests {
use crate::types::mana::ManaCost;
use crate::types::phase::Phase;
use crate::types::player::PlayerId;
use crate::types::triggers::TriggerMode;
use crate::types::triggers::{PlaneswalkRole, TriggerMode};

/// T5 (s25 site 1) — CR 603.7c + CR 608.2c: `concrete_parent_target_filter`
/// binds a `ParentTargetSlot { index }` delayed-condition filter to the
Expand Down Expand Up @@ -852,6 +852,64 @@ mod tests {
);
}

/// CR 603.7c + CR 608.2c: the `parent_target_snapshot` path freezes a
/// MULTI-target parent selection into the delayed ability at creation, exactly
/// as it does for The Pandorica's single target. This is the building-block
/// proof that The Doctor's Childhood Barn's per-opponent "choose up to one
/// target nonland permanent that opponent controls … those permanents phase
/// in" delayed trigger captures every chosen permanent (not just the first).
/// The intervening player ref is harmlessly carried and later filtered out by
/// `collect_phase_in_targets` at fire time.
#[test]
fn parent_target_snapshot_freezes_all_multi_targets_for_delayed_phase_in() {
let mut state = GameState::new_two_player(42);
let obj_a = ObjectId(10);
let obj_b = ObjectId(11);

let inner = AbilityDefinition::new(
AbilityKind::Spell,
Effect::PhaseIn {
target: TargetFilter::ParentTarget,
},
);
let ability = ResolvedAbility::new(
Effect::CreateDelayedTrigger {
condition: DelayedTriggerCondition::WhenNextEvent {
trigger: Box::new(TriggerDefinition::new(TriggerMode::Planeswalked {
role: PlaneswalkRole::Any,
})),
or_trigger: None,
lifetime: crate::types::ability::DelayedTriggerLifetime::Persistent,
},
effect: Box::new(inner),
uses_tracked_set: false,
},
vec![
TargetRef::Object(obj_a),
TargetRef::Player(PlayerId(1)),
TargetRef::Object(obj_b),
],
ObjectId(5),
PlayerId(0),
);

let mut events = Vec::new();
resolve(&mut state, &ability, &mut events).unwrap();

assert_eq!(state.delayed_triggers.len(), 1);
let snapshot = &state.delayed_triggers[0].ability.targets;
assert!(
snapshot.contains(&TargetRef::Object(obj_a)),
"first chosen permanent must be snapshotted, got {snapshot:?}"
);
assert!(
snapshot.contains(&TargetRef::Object(obj_b)),
"second chosen permanent must ALSO be snapshotted (multi-target), got {snapshot:?}"
);
// Persistent lifetime survives across turns until the planeswalk fires.
assert!(state.delayed_triggers[0].one_shot);
}

#[test]
fn parent_target_snapshots_triggering_zone_change_object() {
let mut state = GameState::new_two_player(42);
Expand Down
16 changes: 16 additions & 0 deletions crates/engine/src/game/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,10 @@ fn static_condition_uses_object_population(condition: &StaticCondition) -> bool
| StaticCondition::SourceIsPaired
| StaticCondition::SourceInZone { .. }
| StaticCondition::EnchantedIsFaceDown
// CR 311.2: plane face-up status is command-zone state, never
// battlefield-population-dependent and unperturbed by a battlefield
// entry — `false` exactly like `SourceIsTapped`.
| StaticCondition::SourceIsFaceUp
| StaticCondition::AdditionalCostPaid
| StaticCondition::CastingAsVariant { .. }
| StaticCondition::None => false,
Expand Down Expand Up @@ -1013,6 +1017,10 @@ fn entered_object_perturbs_static_condition(
| StaticCondition::SourceIsPaired
| StaticCondition::SourceInZone { .. }
| StaticCondition::EnchantedIsFaceDown
// CR 311.2: plane face-up status is command-zone state, never
// battlefield-population-dependent and unperturbed by a battlefield
// entry — `false` exactly like `SourceIsTapped`.
| StaticCondition::SourceIsFaceUp
| StaticCondition::AdditionalCostPaid
| StaticCondition::CastingAsVariant { .. }
| StaticCondition::None => false,
Expand Down Expand Up @@ -1231,6 +1239,14 @@ fn evaluate_condition_with_context(
// Callous Oppressor dying while tapped) fails this predicate and any
// `ForAsLongAs { SourceIsTapped }` continuous effect (gain-control, etc.) ends.
StaticCondition::SourceIsTapped => eval_source_is_tapped_on_battlefield(state, source_id),
// CR 311.2 / CR 901.7 / CR 701.31b: the source plane/phenomenon is face up
// iff it is the active plane in the command zone. Planeswalking away turns
// it face down and removes it from the command zone (CR 701.31b), so this
// flips false and any `ForAsLongAs { SourceIsFaceUp }` continuous effect
// (the Barn's "can't phase in ... face up" lock) ends.
StaticCondition::SourceIsFaceUp => {
crate::game::planechase::active_plane(state) == Some(source_id)
}
// CR 110.5b + CR 110.5d: scope-parameterized tap check (the non-source
// sibling of `SourceIsTapped`). Resolve the scope to a concrete object,
// then reuse the same zone-guarded battlefield tap predicate. The parser
Expand Down
9 changes: 5 additions & 4 deletions crates/engine/src/game/planechase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,10 @@ fn queue_planeswalk_trigger(
/// planeswalk: `from == to`); the command zone is never left empty.
///
/// Trigger collection (CR 603.3): the `Planeswalked` event triggers both the
/// departing plane's "planeswalk away from ~" ability (`PlaneswalkedFrom`) and
/// the arriving plane's "planeswalk to / encounter ~" ability
/// (`PlaneswalkedTo`). Both abilities function from the command zone
/// departing plane's "planeswalk away from ~" ability
/// (`Planeswalked { role: From }`) and the arriving plane's
/// "planeswalk to / encounter ~" ability (`Planeswalked { role: To }`). Both
/// abilities function from the command zone
/// (`trigger_zones = [Command]`, stamped by `synthesize_planechase`), and the
/// command-zone trigger scan only inspects objects currently in
/// `state.command_zone`. Because planeswalking removes the departing card from
Expand Down Expand Up @@ -420,7 +421,7 @@ pub fn chaos_ensues(state: &mut GameState, events: &mut Vec<GameEvent>) {
/// card off a planar deck and turn it face up." Encountering a phenomenon is the
/// planeswalk that turns it face up; this entry point performs that planeswalk,
/// which emits the `Planeswalked { to }` event the encounter trigger
/// (`PlaneswalkedTo`) matches.
/// (`Planeswalked { role: To }`) matches.
pub fn encounter(state: &mut GameState, player_id: PlayerId, events: &mut Vec<GameEvent>) {
// CR 312.5: encountering a phenomenon IS this planeswalk — it is the
// turn-based/effect-driven planeswalk that turns the phenomenon face up, NOT
Expand Down
Loading
Loading