From fc78fd043e29992ba0569cae778eab3112506c6e Mon Sep 17 00:00:00 2001 From: Whovencroft <6.60056e+06+Whovencroft@users.noreply.github.com> Date: Thu, 21 May 2026 08:27:59 +0000 Subject: [PATCH] Add cycle trigger: "whenever you cycle" (no object) + fix player filter Two related fixes: 1. Parser: add "whenever you cycle" and "whenever you cycle discard a card" as aliases for TriggerMode::Cycled with valid_target=Controller. - "whenever you cycle" is the Amonkhet/MH2 shorthand (no explicit object) - "whenever you cycle discard a card" is the pre-Amonkhet wording Both map identically to the existing "whenever you cycle a card" arm. Unlocks 14 + 11 = 25 cards (with overlap: 14 unique new cards). 2. Runtime: fix match_cycled and match_cycled_or_discarded to check valid_player_matches against GameEvent::Cycled.player_id. Previously the player filter (valid_target=Controller) was parsed but silently ignored at runtime, causing triggers to fire for any player cycling. Same fix applied to match_cycled_or_discarded for the Discarded branch. Cards unlocked (sample): Flameblade Adept, Hekma Sentinels, Ruthless Sniper, Horror of the Broken Lands, Faith of the Devoted, Pitiless Vizier, Ominous Sphinx, Zenith Seeker, Archfiend of Ifnir, Drake Haven, Grisly Survivor, Cunning Survivor, Curator of Mysteries, Shadowstorm Vizier. --- crates/engine/src/game/trigger_matchers.rs | 21 +++++++++++--- crates/engine/src/parser/oracle_trigger.rs | 32 +++++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/game/trigger_matchers.rs b/crates/engine/src/game/trigger_matchers.rs index 396559b44a..b51aee91d6 100644 --- a/crates/engine/src/game/trigger_matchers.rs +++ b/crates/engine/src/game/trigger_matchers.rs @@ -1684,8 +1684,13 @@ pub(super) fn match_cycled( source_id: ObjectId, state: &GameState, ) -> bool { - if let GameEvent::Cycled { object_id, .. } = event { - valid_card_matches(trigger, state, *object_id, source_id) + if let GameEvent::Cycled { + player_id, + object_id, + } = event + { + valid_player_matches(trigger, state, *player_id, source_id) + && valid_card_matches(trigger, state, *object_id, source_id) } else { false } @@ -1700,8 +1705,16 @@ pub(super) fn match_cycled_or_discarded( state: &GameState, ) -> bool { match event { - GameEvent::Cycled { object_id, .. } | GameEvent::Discarded { object_id, .. } => { - valid_card_matches(trigger, state, *object_id, source_id) + GameEvent::Cycled { + player_id, + object_id, + } + | GameEvent::Discarded { + player_id, + object_id, + } => { + valid_player_matches(trigger, state, *player_id, source_id) + && valid_card_matches(trigger, state, *object_id, source_id) } _ => false, } diff --git a/crates/engine/src/parser/oracle_trigger.rs b/crates/engine/src/parser/oracle_trigger.rs index 6131c2a895..621129c830 100644 --- a/crates/engine/src/parser/oracle_trigger.rs +++ b/crates/engine/src/parser/oracle_trigger.rs @@ -5814,7 +5814,15 @@ fn try_parse_player_trigger(lower: &str) -> Option<(TriggerMode, TriggerDefiniti return Some((TriggerMode::Cycled, def)); } - if matches!(lower, "whenever you cycle a card" | "when you cycle a card") { + if matches!( + lower, + "whenever you cycle a card" + | "when you cycle a card" + | "whenever you cycle" + | "when you cycle" + | "whenever you cycle discard a card" + | "when you cycle discard a card" + ) { let mut def = make_base(); def.mode = TriggerMode::Cycled; def.valid_target = Some(TargetFilter::Controller); @@ -17876,4 +17884,26 @@ mod snapshot_tests { ); } } + + #[test] + fn trigger_you_cycle() { + let def = parse_trigger_line( + "Whenever you cycle, ~ gets +1/+0 until end of turn.", + "Flameblade Adept", + ); + assert_eq!(def.mode, TriggerMode::Cycled); + assert_eq!(def.valid_target, Some(TargetFilter::Controller)); + assert!(def.valid_card.is_none()); + } + + #[test] + fn trigger_you_cycle_discard_a_card() { + let def = parse_trigger_line( + "Whenever you cycle discard a card, ~ gets +1/+0 until end of turn.", + "Flameblade Adept", + ); + assert_eq!(def.mode, TriggerMode::Cycled); + assert_eq!(def.valid_target, Some(TargetFilter::Controller)); + assert!(def.valid_card.is_none()); + } }