diff --git a/crates/engine/src/game/effects/counters.rs b/crates/engine/src/game/effects/counters.rs index 536ceb7b0d..243b45768d 100644 --- a/crates/engine/src/game/effects/counters.rs +++ b/crates/engine/src/game/effects/counters.rs @@ -1977,6 +1977,23 @@ fn resolve_defined_or_targets( .collect(); } + // CR 608.2c: A `ParentTarget` in a chained counter effect refers to the + // object chosen by the parent instruction. Prefer that propagated object + // target over the triggering event context; for a landfall trigger, the + // latter is the entering land rather than the creature chosen by the + // player. The object guard deliberately excludes the chooser-only target + // bookkeeping used by `ChooseOneOf` branches above. + if matches!(target_spec, Some(TargetFilter::ParentTarget)) && has_object_target { + return ability + .live_object_targets(state) + .into_iter() + .filter_map(|target| match target { + TargetRef::Object(id) => Some(id), + TargetRef::Player(_) => None, + }) + .collect(); + } + if let Some(filter) = target_spec { let event_targets = crate::game::targeting::resolve_event_context_targets(state, filter, ability.source_id); diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index 83160706a4..7fe6c1febd 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -896,6 +896,7 @@ mod scarab_god_regression; mod scholarship_sponsor; mod screaming_nemesis_combat_damage_multi_trigger; mod screaming_nemesis_life_lock; +mod scythecat_cub_second_resolution; mod search_delivery_observer_dedup; mod season_points_budget_modal; mod seasoned_dungeoneer_initiative_room_trigger; diff --git a/crates/engine/tests/integration/scythecat_cub_second_resolution.rs b/crates/engine/tests/integration/scythecat_cub_second_resolution.rs new file mode 100644 index 0000000000..7b3bb10698 --- /dev/null +++ b/crates/engine/tests/integration/scythecat_cub_second_resolution.rs @@ -0,0 +1,96 @@ +//! Scythecat Cub doubles the counters placed by its second landfall resolution. + +use engine::game::scenario::{GameScenario, P0}; +use engine::types::ability::TargetRef; +use engine::types::actions::GameAction; +use engine::types::counter::CounterType; +use engine::types::game_state::WaitingFor; +use engine::types::phase::Phase; + +const SCYTHECAT_CUB_ORACLE: &str = "Trample\n\ +Landfall — Whenever a land you control enters, put a +1/+1 counter on target creature you control. If this is the second time this ability has resolved this turn, double the number of +1/+1 counters on that creature instead."; + +fn resolve_cub_landfall( + runner: &mut engine::game::scenario::GameRunner, + land_id: engine::types::identifiers::ObjectId, + target_id: engine::types::identifiers::ObjectId, +) { + let card_id = runner.state().objects[&land_id].card_id; + runner + .act(GameAction::PlayLand { + object_id: land_id, + card_id, + }) + .expect("land should be playable"); + + match &runner.state().waiting_for { + WaitingFor::TriggerTargetSelection { target_slots, .. } => { + assert!( + target_slots[0] + .legal_targets + .iter() + .any(|target| matches!(target, TargetRef::Object(id) if *id == target_id)), + "Lotus Cobra must be a legal Scythecat Cub target" + ); + } + other => panic!("expected Scythecat Cub target selection, got {other:?}"), + } + + runner + .act(GameAction::ChooseTarget { + target: Some(TargetRef::Object(target_id)), + }) + .expect("Lotus Cobra should be selectable"); + runner.advance_until_stack_empty(); +} + +#[test] +fn scythecat_cub_doubles_counters_on_second_landfall_resolution() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + let cub_id = scenario + .add_creature_from_oracle(P0, "Scythecat Cub", 2, 2, SCYTHECAT_CUB_ORACLE) + .id(); + let cobra_id = scenario.add_creature(P0, "Lotus Cobra", 2, 1).id(); + let first_land_id = scenario.add_land_to_hand(P0, "First Land").id(); + let second_land_id = scenario.add_land_to_hand(P0, "Second Land").id(); + + let mut runner = scenario.build(); + runner.state_mut().max_lands_per_turn = 2; + runner + .state_mut() + .objects + .get_mut(&cobra_id) + .expect("Lotus Cobra should start on the battlefield") + .counters + .insert(CounterType::Plus1Plus1, 1); + + resolve_cub_landfall(&mut runner, first_land_id, cobra_id); + assert_eq!( + runner.state().objects[&cobra_id] + .counters + .get(&CounterType::Plus1Plus1) + .copied(), + Some(2), + "the first landfall resolution should place one +1/+1 counter on the seeded target" + ); + + resolve_cub_landfall(&mut runner, second_land_id, cobra_id); + assert_eq!( + runner.state().objects[&cobra_id] + .counters + .get(&CounterType::Plus1Plus1) + .copied(), + Some(4), + "the second landfall resolution should double Lotus Cobra's counters" + ); + assert_eq!( + runner + .state() + .ability_resolutions_this_turn + .get(&(cub_id, 0)), + Some(&2), + "both landfall triggers should share Cub's printed ability index" + ); +}