Skip to main content

manabrew_engine/trigger/
trigger.rs

1use std::fmt;
2use std::hash::{Hash, Hasher};
3
4use dyn_clone::DynClone;
5use forge_foundation::{PhaseType, ZoneType};
6use serde::{Deserialize, Serialize};
7
8use crate::ability::AbilityKey;
9use crate::card::{valid_filter, Card};
10use crate::card_trait_base::{CardTrait, CardTraitBase, CardTraitIrOwner, MatchValidTarget};
11use crate::core::HasSVars;
12use crate::event::{AbilityValue, RunParams};
13use crate::game::GameState;
14use crate::game_loop::trigger_replacement_base::TriggerReplacementBase;
15use crate::ids::{CardId, PlayerId};
16use crate::parsing::{keys, CompiledSelector, Params};
17use crate::player::PlayerCollection;
18use crate::spellability::{build_spell_ability, SpellAbility, TriggerCondition};
19use crate::trigger::trigger_ir::TriggerIr;
20use crate::trigger::TriggerType;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Trigger {
24    pub id: u32,
25    #[serde(skip, default)]
26    pub base: TriggerReplacementBase,
27    /// Cheap discriminant for pattern-matching (replaces `matches!(t.mode, TriggerMode::X { .. })`).
28    #[serde(default)]
29    pub kind: TriggerType,
30    pub mode: Box<dyn TriggerBehavior>,
31    #[serde(skip, default)]
32    pub ir: TriggerIr,
33    pub execute: String,
34    pub optional: bool,
35    pub description: String,
36    pub static_trigger: bool,
37    #[serde(default)]
38    pub trigger_remembered: Vec<AbilityValue>,
39    #[serde(default)]
40    pub spawning_ability: Option<SpellAbility>,
41}
42
43impl PartialEq for Trigger {
44    fn eq(&self, other: &Self) -> bool {
45        self.id == other.id
46    }
47}
48
49impl Eq for Trigger {}
50
51impl Hash for Trigger {
52    fn hash<H: Hasher>(&self, state: &mut H) {
53        "Trigger".hash(state);
54        self.id.hash(state);
55    }
56}
57
58impl fmt::Display for Trigger {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(f, "{}", self.to_string_with_active(false))
61    }
62}
63
64#[typetag::serde(tag = "type")]
65pub trait TriggerBehavior: fmt::Debug + DynClone + Send + Sync {
66    fn trigger_type(&self) -> TriggerType;
67    /// Java's `Trigger.performTest()`. The `trigger` parameter provides access
68    /// to the parent `Trigger`'s params, base, and host card — equivalent of
69    /// Java's `this` in the trigger subclass.
70    fn perform_test(&self, trigger: &Trigger, run_params: &RunParams, game: &GameState) -> bool;
71    fn set_triggering_objects(
72        &self,
73        trigger: &Trigger,
74        sa: &mut SpellAbility,
75        params: &RunParams,
76        game: &GameState,
77    );
78    fn get_important_stack_objects(&self, trigger: &Trigger, sa: &SpellAbility) -> String;
79    fn origin_zone(&self) -> Option<ZoneType> {
80        None
81    }
82    fn destination_zone(&self) -> Option<ZoneType> {
83        None
84    }
85    fn drawn_number(&self) -> Option<i32> {
86        None
87    }
88}
89
90dyn_clone::clone_trait_object!(TriggerBehavior);
91
92impl Trigger {
93    pub fn get_id(&self) -> u32 {
94        self.id
95    }
96
97    pub fn set_id(&mut self, id: u32) {
98        self.id = id;
99        self.base.card_trait_base.set_id(id as i32);
100    }
101
102    /// Returns the host card's ID. Mirrors Java's `getHostCard().getId()`.
103    pub fn host_card_id(&self) -> CardId {
104        self.base.card_trait_base.host_card_id()
105    }
106
107    /// Returns the host card's current controller from live game state.
108    /// Use this instead of the stored host card's controller, which may be stale.
109    pub fn host_controller(&self, game: &GameState) -> PlayerId {
110        game.card(self.host_card_id()).controller
111    }
112
113    pub fn get_mode(&self) -> &dyn TriggerBehavior {
114        &*self.mode
115    }
116
117    pub fn origin_zone(&self) -> Option<ZoneType> {
118        self.ir.origin_zone
119    }
120
121    pub fn destination_zone(&self) -> Option<ZoneType> {
122        self.ir.destination_zone
123    }
124
125    pub fn set_mode(&mut self, mode: Box<dyn TriggerBehavior>) {
126        self.kind = mode.trigger_type();
127        self.mode = mode;
128    }
129
130    pub fn get_trigger_remembered(&self) -> &[AbilityValue] {
131        &self.trigger_remembered
132    }
133
134    pub fn bind_host_card_id(&mut self, host_card: CardId) {
135        self.base.set_host_card_id(host_card);
136    }
137
138    pub fn get_active_zone(&self) -> &[ZoneType] {
139        self.base.get_active_zone().unwrap_or(&[])
140    }
141
142    pub fn set_active_zone(&mut self, zones: Vec<ZoneType>) {
143        self.base.set_active_zone(zones);
144    }
145
146    pub fn is_intrinsic(&self) -> bool {
147        self.base.card_trait_base.is_intrinsic()
148    }
149
150    pub fn set_intrinsic(&mut self, intrinsic: bool) {
151        self.base.card_trait_base.set_intrinsic(intrinsic);
152    }
153
154    pub fn get_overriding_ability(&self) -> Option<&SpellAbility> {
155        self.base.get_overriding_ability()
156    }
157
158    pub fn set_overriding_ability(&mut self, mut overriding_ability: SpellAbility) {
159        overriding_ability.is_trigger = true;
160        overriding_ability.source_trigger_id = Some(self.id);
161        self.base.set_overriding_ability(overriding_ability);
162    }
163
164    pub fn get_spawning_ability(&self) -> Option<&SpellAbility> {
165        self.spawning_ability.as_ref()
166    }
167
168    pub fn set_spawning_ability(&mut self, ability: SpellAbility) {
169        self.spawning_ability = Some(ability);
170    }
171}
172
173/// Mirrors Java's `Trigger extends CardTraitBase` override of `matchesValid`:
174/// when `this instanceof Trigger` and a spawning ability is present, the
175/// source player resolves to the spawning ability's activating player rather
176/// than the source card's controller.
177impl CardTrait for Trigger {
178    fn base(&self) -> &CardTraitBase {
179        &self.base.card_trait_base
180    }
181
182    fn resolve_source_player(&self, src_card: &Card) -> PlayerId {
183        self.spawning_ability
184            .as_ref()
185            .map(|sa| sa.activating_player)
186            .unwrap_or(src_card.controller)
187    }
188
189    fn matches_compiled_valid(
190        &self,
191        target: &MatchValidTarget<'_>,
192        selector: &CompiledSelector,
193        src_card: Option<&Card>,
194    ) -> bool {
195        let Some(src) = src_card else {
196            return false;
197        };
198        let player = self.resolve_source_player(src);
199        match target {
200            MatchValidTarget::Card(card) => {
201                let trigger_remembered_cards = self
202                    .trigger_remembered
203                    .iter()
204                    .filter_map(|value| match value {
205                        AbilityValue::Card(card_id) => Some(*card_id),
206                        _ => None,
207                    })
208                    .collect::<Vec<_>>();
209                valid_filter::matches_valid_card_selector_with_context(
210                    selector,
211                    card,
212                    valid_filter::MatchContext::from_source(src)
213                        .with_source_controller(player)
214                        .with_trigger_remembered_cards(&trigger_remembered_cards),
215                )
216            }
217            _ => self
218                .base()
219                .matches_compiled_valid_with_player(target, selector, src, player),
220        }
221    }
222}
223
224impl CardTraitIrOwner for Trigger {
225    type Ir = TriggerIr;
226
227    fn ir(&self) -> &Self::Ir {
228        &self.ir
229    }
230
231    fn card_trait_requirements(&self) -> &valid_filter::CardTraitRequirementsIr {
232        &self.ir.card_trait_requirements
233    }
234}
235
236impl Trigger {
237    pub fn set_trigger_phases(&mut self, phases: Vec<PhaseType>) {
238        self.ir.valid_phases = Some(phases);
239    }
240
241    /// Java parity shim for Trigger.resetIDs().
242    pub fn reset_i_ds(next_id: &mut u32) {
243        *next_id = 50_000;
244    }
245
246    pub fn to_string_with_active(&self, active: bool) -> String {
247        if self.ir.trigger_description.is_none() || self.base.card_trait_base.is_suppressed() {
248            return String::new();
249        }
250
251        let mut desc = self.ir.trigger_description.clone().unwrap_or_default();
252
253        if !desc.contains("ABILITY") {
254            let host_name = self.base.card_trait_base.host_card_id().0.to_string();
255            desc = desc.replace("CARDNAME", &host_name);
256            desc = desc.replace("NICKNAME", &host_name);
257            if desc.contains("ORIGINALHOST") {
258                let original_host = self
259                    .get_overriding_ability()
260                    .and_then(|sa| sa.original_host)
261                    .map(|id| id.0.to_string())
262                    .unwrap_or_default();
263                desc = desc.replace("ORIGINALHOST", &original_host);
264            }
265        }
266
267        if desc.contains("EFFECTSOURCE") {
268            let replacement = if active {
269                self.base.card_trait_base.host_card_id().0.to_string()
270            } else {
271                self.base.card_trait_base.host_card_id().0.to_string()
272            };
273            desc = desc.replace("EFFECTSOURCE", &replacement);
274        }
275
276        if !self.trigger_remembered.is_empty() {
277            desc.push_str(&format!(" ({:?})", self.trigger_remembered));
278        }
279
280        desc
281    }
282
283    /// Minimal `ABILITY` replacement parity used by stack text paths.
284    pub fn replace_ability_text(
285        &self,
286        desc: &str,
287        game: &GameState,
288        host_card: CardId,
289        activating_player: PlayerId,
290    ) -> String {
291        self.replace_ability_text_for_stack(desc, None, false, game, host_card, activating_player)
292    }
293
294    pub fn replace_ability_text_with_ability(
295        &self,
296        desc: &str,
297        sa: Option<SpellAbility>,
298        game: &GameState,
299        host_card: CardId,
300        activating_player: PlayerId,
301    ) -> String {
302        self.replace_ability_text_for_stack(desc, sa, false, game, host_card, activating_player)
303    }
304
305    pub fn replace_ability_text_for_stack(
306        &self,
307        desc: &str,
308        mut sa: Option<SpellAbility>,
309        _for_stack: bool,
310        game: &GameState,
311        host_card: CardId,
312        activating_player: PlayerId,
313    ) -> String {
314        if !desc.contains("ABILITY") {
315            return desc.to_string();
316        }
317
318        if sa.is_none() {
319            sa = self
320                .ensure_ability(game, host_card, activating_player)
321                .or_else(|| self.get_overriding_ability().cloned());
322        }
323
324        let Some(mut sa) = sa else {
325            return desc.to_string();
326        };
327
328        let mut sa_desc = String::new();
329        let mut dig_more = true;
330
331        if sa.is_wrapper() {
332            let wrapped = sa.get_wrapped_ability().clone();
333            sa = wrapped;
334
335            if sa.api == Some(crate::ability::api_type::ApiType::Charm) {
336                sa_desc = sa.stack_description.clone();
337                dig_more = false;
338            }
339        }
340
341        if dig_more {
342            let mut trig_sa = Some(sa.clone());
343            while let Some(current) = trig_sa {
344                match current.api {
345                    Some(crate::ability::api_type::ApiType::Charm) => {
346                        let source_id = current.source.unwrap_or(host_card);
347                        let choices = current.ir.choices.as_deref().unwrap_or("");
348                        sa_desc = crate::ability::effects::charm_effect::make_formated_description(
349                            game, source_id, choices,
350                        );
351                        break;
352                    }
353                    Some(crate::ability::api_type::ApiType::ImmediateTrigger)
354                    | Some(crate::ability::api_type::ApiType::DelayedTrigger) => {
355                        trig_sa = current.get_additional_ability("Execute").cloned();
356                    }
357                    _ => {
358                        trig_sa = current.get_sub_ability().cloned();
359                    }
360                }
361            }
362        }
363
364        if sa_desc.is_empty() {
365            sa_desc = sa.to_string();
366        }
367
368        sa_desc = sa_desc.trim().to_string();
369        if !sa_desc.is_empty() {
370            let host_name = game.card(host_card).card_name.clone();
371            if !sa_desc.starts_with(&host_name) {
372                let mut chars = sa_desc.chars();
373                if let Some(first) = chars.next() {
374                    sa_desc = first.to_lowercase().collect::<String>() + chars.as_str();
375                }
376            }
377            if sa_desc.contains("ORIGINALHOST") {
378                let original_host = sa
379                    .original_host
380                    .map(|id| game.card(id).card_name.clone())
381                    .unwrap_or_default();
382                sa_desc = sa_desc.replace("ORIGINALHOST", &original_host);
383            }
384        } else {
385            sa_desc = "<take no action>".to_string();
386        }
387
388        let mut result = desc.replace("ABILITY", &sa_desc);
389        let translated_name = game.card(host_card).card_name.clone();
390        result = result.replace("CARDNAME", &translated_name);
391        result = result.replace("NICKNAME", &translated_name);
392        result
393    }
394
395    /// Mirrors Java Trigger.phasesCheck() for common phase/turn params.
396    pub fn phases_check(
397        &self,
398        game: &GameState,
399        host_card: CardId,
400        event_phase: Option<PhaseType>,
401    ) -> bool {
402        let phase = event_phase.unwrap_or(game.turn.phase);
403        let host_controller = game.card(host_card).controller;
404
405        if let Some(valid_phases) = self.ir.valid_phases.as_ref() {
406            if !valid_phases.contains(&phase) {
407                return false;
408            }
409            if let Some(expected) = self.ir.phase_count {
410                let current = if phase == PhaseType::Main2 { 2 } else { 1 };
411                if current != expected {
412                    return false;
413                }
414            }
415        }
416
417        if self.ir.player_turn && game.turn.active_player != host_controller {
418            return false;
419        }
420        if self.ir.not_player_turn && game.turn.active_player == host_controller {
421            return false;
422        }
423        if self.ir.opponent_turn {
424            let active = game.turn.active_player;
425            let is_opponent_turn = active != host_controller;
426            if !is_opponent_turn {
427                return false;
428            }
429        }
430        if self.ir.first_upkeep && !(phase == PhaseType::Upkeep && game.turn.turn_number >= 1) {
431            return false;
432        }
433        if self.ir.first_upkeep_this_game
434            && !(phase == PhaseType::Upkeep && game.turn.turn_number == 1)
435        {
436            return false;
437        }
438        if self.ir.first_combat && phase != PhaseType::CombatBegin {
439            return false;
440        }
441        if let Some(expected) = self.ir.turn_count {
442            if game.turn.turn_number != expected {
443                return false;
444            }
445        }
446        true
447    }
448
449    /// Mirrors Java Trigger.requirementsCheck() subset used in current engine.
450    pub fn requirements_check(&self, game: &GameState, host_card: CardId) -> bool {
451        if self.ir.a_player_has_more_life_than_each_other {
452            let mut highest = i32::MIN;
453            let mut count = 0;
454            for p in &game.players {
455                if p.life > highest {
456                    highest = p.life;
457                    count = 1;
458                } else if p.life == highest {
459                    count += 1;
460                }
461            }
462            if count != 1 {
463                return false;
464            }
465        }
466        if self.ir.a_player_has_most_cards_in_hand {
467            let mut largest = i32::MIN;
468            let mut count = 0;
469            for p in &game.players {
470                let hand_count = game.cards_in_zone(ZoneType::Hand, p.id).len() as i32;
471                if hand_count > largest {
472                    largest = hand_count;
473                    count = 1;
474                } else if hand_count == largest {
475                    count += 1;
476                }
477            }
478            if count != 1 {
479                return false;
480            }
481        }
482        let host = game.card(host_card);
483        if !self.meets_card_trait_requirements(game, host, &self.base.card_trait_base) {
484            return false;
485        }
486        self.check_resolved_limit(game, host_card)
487    }
488
489    /// Mirrors Java Trigger.checkResolvedLimit() (approximation with per-card counter).
490    pub fn check_resolved_limit(&self, game: &GameState, host_card: CardId) -> bool {
491        if let Some(limit) = self.ir.resolved_limit {
492            if self.get_overriding_ability().is_none() {
493                let Some(ability_text) = game.card(host_card).get_s_var(&self.execute) else {
494                    return true;
495                };
496                let ability = build_spell_ability(
497                    game,
498                    host_card,
499                    ability_text,
500                    game.card(host_card).controller,
501                );
502                return (game
503                    .card(host_card)
504                    .get_ability_resolved_this_turn_activators(Some(&ability))
505                    .len() as u32)
506                    < limit;
507            }
508            return (game
509                .card(host_card)
510                .get_ability_resolved_this_turn_activators(self.get_overriding_ability())
511                .len() as u32)
512                < limit;
513        }
514        true
515    }
516
517    /// Mirrors Java Trigger.checkActivationLimit().
518    pub fn check_activation_limit(&self, game: &GameState, host_card: CardId) -> bool {
519        if let Some(limit) = self.ir.activation_limit {
520            if self.get_activations_this_turn(game, host_card) >= limit {
521                return false;
522            }
523        }
524        if let Some(limit) = self.ir.game_activation_limit {
525            let used = self.get_activations_this_game(game, host_card);
526            if used >= limit {
527                return false;
528            }
529        }
530        true
531    }
532
533    pub fn get_activations_this_turn(&self, game: &GameState, host_card: CardId) -> u32 {
534        if self.get_overriding_ability().is_some() {
535            return game
536                .card(host_card)
537                .get_ability_activated_this_turn(self.get_overriding_ability());
538        }
539        0
540    }
541
542    /// Polymorphic `Valid...` check for optional card payloads.
543    /// Mirrors Java's `matchesValidParam` flow by routing through `CardTrait`.
544    pub fn matches_optional_valid_card_filter(
545        &self,
546        filter: &Option<CompiledSelector>,
547        card_id: Option<CardId>,
548        game: &GameState,
549    ) -> bool {
550        match (filter.as_ref(), card_id) {
551            (None, _) => true,
552            (Some(_), None) => false,
553            (Some(selector), Some(card_id)) => {
554                let src = self.base.card_trait_base.host_card(game);
555                let player = self.resolve_source_player(src);
556                let trigger_remembered_cards = self
557                    .trigger_remembered
558                    .iter()
559                    .filter_map(|value| match value {
560                        AbilityValue::Card(card_id) => Some(*card_id),
561                        _ => None,
562                    })
563                    .collect::<Vec<_>>();
564                valid_filter::matches_valid_card_selector_with_context(
565                    selector,
566                    game.card(card_id),
567                    valid_filter::MatchContext::from_source(src)
568                        .with_game(game)
569                        .with_source_controller(player)
570                        .with_trigger_remembered_cards(&trigger_remembered_cards),
571                )
572            }
573        }
574    }
575
576    /// Polymorphic `Valid...` check for optional player payloads.
577    /// Mirrors Java's `matchesValidParam` flow by routing through `CardTrait`.
578    pub fn matches_optional_valid_player_filter(
579        &self,
580        filter: &Option<CompiledSelector>,
581        player_id: Option<PlayerId>,
582        game: &GameState,
583    ) -> bool {
584        match (filter.as_ref(), player_id) {
585            (None, _) => true,
586            (Some(_), None) => false,
587            (Some(selector), Some(player_id)) => self.matches_compiled_valid(
588                &MatchValidTarget::Player(player_id),
589                selector,
590                Some(self.base.card_trait_base.host_card(game)),
591            ),
592        }
593    }
594
595    /// Matches a required card against a `Valid...` card filter from this trigger's host context.
596    pub fn matches_valid_card_filter(
597        &self,
598        filter: &CompiledSelector,
599        card_id: CardId,
600        game: &GameState,
601    ) -> bool {
602        let src = self.base.card_trait_base.host_card(game);
603        let player = self.resolve_source_player(src);
604        let trigger_remembered_cards = self
605            .trigger_remembered
606            .iter()
607            .filter_map(|value| match value {
608                AbilityValue::Card(card_id) => Some(*card_id),
609                _ => None,
610            })
611            .collect::<Vec<_>>();
612        valid_filter::matches_valid_card_selector_with_context(
613            filter,
614            game.card(card_id),
615            valid_filter::MatchContext::from_source(src)
616                .with_game(game)
617                .with_source_controller(player)
618                .with_trigger_remembered_cards(&trigger_remembered_cards),
619        )
620    }
621
622    /// Matches a required player against a `Valid...` player filter from this trigger's host context.
623    pub fn matches_valid_player_filter(
624        &self,
625        filter: &CompiledSelector,
626        player: PlayerId,
627        game: &GameState,
628    ) -> bool {
629        self.matches_compiled_valid(
630            &MatchValidTarget::Player(player),
631            filter,
632            Some(self.base.card_trait_base.host_card(game)),
633        )
634    }
635
636    /// Matches a required player against a `Valid...` player filter from an explicit source controller.
637    pub fn matches_valid_player_filter_with_controller(
638        &self,
639        filter: &CompiledSelector,
640        player: PlayerId,
641        source_controller: PlayerId,
642    ) -> bool {
643        valid_filter::matches_valid_player_selector(filter, player, source_controller)
644    }
645
646    /// Matches an optional counter-type filter.
647    pub fn matches_counter_type_filter(expected: &Option<String>, actual: &Option<String>) -> bool {
648        if let Some(expected) = expected {
649            if let Some(actual) = actual {
650                actual.eq_ignore_ascii_case(expected)
651            } else {
652                false
653            }
654        } else {
655            true
656        }
657    }
658
659    /// Matches zone filters (origin and/or destination).
660    pub fn matches_zone_filter(expected: &Option<ZoneType>, actual: Option<ZoneType>) -> bool {
661        if let Some(expected) = expected {
662            actual == Some(*expected)
663        } else {
664            true
665        }
666    }
667
668    /// Matches spell-ability filter tokens.
669    pub fn matches_valid_sa_filter(
670        &self,
671        filter: &str,
672        sa: &crate::spellability::SpellAbility,
673    ) -> bool {
674        let f = filter.trim();
675        if f.is_empty() {
676            return true;
677        }
678        if f.eq_ignore_ascii_case("Spell") {
679            return sa.is_spell;
680        }
681        if f.eq_ignore_ascii_case("Ability") {
682            return !sa.is_spell;
683        }
684        true
685    }
686
687    /// Matches a damage target filter for either player or card targets.
688    pub fn matches_damage_target_filter(
689        &self,
690        filter: &Option<CompiledSelector>,
691        run_params: &RunParams,
692        game: &GameState,
693        strict_card_filter: bool,
694    ) -> bool {
695        let filter = match filter {
696            Some(f) => f,
697            None => return true,
698        };
699        if let Some(target_card) = run_params.damage_target_card {
700            self.matches_valid_card_filter(filter, target_card, game)
701        } else if strict_card_filter {
702            let raw = filter.as_raw();
703            let is_card_filter = raw.starts_with("Card.")
704                || raw.starts_with("Creature.")
705                || raw.starts_with("Permanent.")
706                || raw.starts_with("Artifact.")
707                || raw.starts_with("Enchantment.")
708                || raw.starts_with("Planeswalker.");
709            if is_card_filter {
710                false
711            } else if let Some(target_player) = run_params.damage_target_player {
712                self.matches_valid_player_filter(filter, target_player, game)
713            } else {
714                false
715            }
716        } else if let Some(target_player) = run_params.damage_target_player {
717            self.matches_valid_player_filter(filter, target_player, game)
718        } else {
719            false
720        }
721    }
722
723    pub fn get_activations_this_game(&self, game: &GameState, host_card: CardId) -> u32 {
724        if self.get_overriding_ability().is_some() {
725            return game
726                .card(host_card)
727                .get_ability_activated_this_game(self.get_overriding_ability());
728        }
729        0
730    }
731
732    /// Mirrors Java Trigger.meetsRequirementsOnTriggeredObjects() subset.
733    pub fn meets_requirements_on_triggered_objects(
734        &self,
735        game: &GameState,
736        run_params: &RunParams,
737        host_card: CardId,
738    ) -> bool {
739        let condition = self.ir.condition.as_ref();
740
741        if self
742            .base
743            .card_trait_base
744            .is_keyword(crate::keyword::keyword_instance::Keyword::Evolve)
745            || matches!(condition, Some(TriggerCondition::Evolve))
746        {
747            let Some(moved) = run_params.card else {
748                return false;
749            };
750            let moved_card = game.card(moved);
751            let host = game.card(host_card);
752            if !moved_card.is_creature() || !host.is_creature() {
753                return false;
754            }
755            if moved_card.power() <= host.power() && moved_card.toughness() <= host.toughness() {
756                return false;
757            }
758        }
759
760        let Some(condition) = condition else {
761            return true;
762        };
763
764        match condition {
765            TriggerCondition::LifePaid => {
766                if let Some(sa) = run_params.spell_ability.as_ref() {
767                    sa.get_amount_life_paid() > 0
768                } else {
769                    true
770                }
771            }
772            TriggerCondition::NoOpponentHasMoreLifeThanAttacked => {
773                let attacked = run_params
774                    .get_player(AbilityKey::Attacked)
775                    .or_else(|| run_params.get_player(AbilityKey::Defender));
776                let Some(attacked_player) = attacked else {
777                    return false;
778                };
779                let life = game.player(attacked_player).life;
780                !PlayerCollection::opponents_of(game, game.card(host_card).controller)
781                    .into_iter()
782                    .filter(|opp| *opp != attacked_player)
783                    .any(|opp| game.player(opp).life > life)
784            }
785            TriggerCondition::Sacrificed => run_params
786                .spell_ability
787                .as_ref()
788                .map(|sa| {
789                    !sa.paid_hash
790                        .get("Sacrificed")
791                        .cloned()
792                        .unwrap_or_default()
793                        .is_empty()
794                })
795                .unwrap_or(true),
796            TriggerCondition::AttackedPlayerWithMostLife => {
797                let attacked = run_params
798                    .get_player(AbilityKey::Attacked)
799                    .or_else(|| run_params.get_player(AbilityKey::Defender));
800                let Some(attacked_player) = attacked else {
801                    return false;
802                };
803                let attacked_life = game.player(attacked_player).life;
804                game.alive_players()
805                    .into_iter()
806                    .all(|pid| game.player(pid).life <= attacked_life)
807            }
808            TriggerCondition::AttackerHasUnattackedOpp => {
809                let Some(attacking_player) = run_params.attacking_player else {
810                    return false;
811                };
812                let attacked_this_combat =
813                    &game.player(attacking_player).attacked_players_this_combat;
814                !PlayerCollection::opponents_of(game, attacking_player)
815                    .into_iter()
816                    .all(|opp| attacked_this_combat.contains(&opp))
817            }
818            TriggerCondition::Evolve | TriggerCondition::Unsupported(_) => true,
819        }
820    }
821
822    pub fn add_remembered<T: Into<AbilityValue>>(&mut self, item: T) {
823        self.trigger_remembered.push(item.into());
824    }
825
826    pub fn is_static(&self) -> bool {
827        self.static_trigger
828    }
829
830    /// Mirrors Java's `Trigger.isManaAbility()`.
831    /// A trigger is a mana ability only if its mode is TapsForMana or ManaAdded
832    /// AND the resulting SpellAbility itself is a mana ability (has a mana part).
833    pub fn is_mana_ability(
834        &self,
835        game: &GameState,
836        host_card: CardId,
837        activating_player: PlayerId,
838    ) -> bool {
839        if !matches!(self.kind, TriggerType::TapsForMana | TriggerType::ManaAdded) {
840            return false;
841        }
842        self.ensure_ability(game, host_card, activating_player)
843            .is_some_and(|sa| sa.is_mana_ability)
844    }
845
846    pub fn add_remembered_many<T: Into<AbilityValue>, I: IntoIterator<Item = T>>(
847        &mut self,
848        items: I,
849    ) {
850        for item in items {
851            self.add_remembered(item);
852        }
853    }
854
855    pub fn copy(&self, next_id: &mut u32, keep_id: bool) -> Self {
856        let mut out = self.clone();
857        if !keep_id {
858            out.id = *next_id;
859            *next_id = next_id.saturating_add(1);
860        }
861        out.base.card_trait_base.set_id(out.id as i32);
862        out
863    }
864
865    pub fn copy_to_host(
866        &self,
867        new_host: crate::card::Card,
868        lki: bool,
869        keep_text_changes: bool,
870        spell_ability: Option<SpellAbility>,
871        next_id: &mut u32,
872    ) -> Self {
873        let mut copy = self.clone();
874        self.base.card_trait_base.copy_helper_with_text(
875            &mut copy.base.card_trait_base,
876            new_host.clone(),
877            lki || keep_text_changes,
878        );
879
880        if let Some(spell_ability) = spell_ability {
881            copy.set_overriding_ability(spell_ability);
882        } else if let Some(overriding_ability) = self.get_overriding_ability() {
883            copy.set_overriding_ability(
884                overriding_ability.copy_with_host_lki(new_host.clone(), lki),
885            );
886        }
887
888        if !lki {
889            copy.set_id(*next_id);
890            *next_id = next_id.saturating_add(1);
891        }
892
893        if let Some(valid_phases) = self.ir.valid_phases.clone() {
894            copy.set_trigger_phases(valid_phases);
895        }
896        copy.base.valid_host_zones = self.base.valid_host_zones.clone();
897        copy
898    }
899
900    /// Tracks trigger activation on the host card.
901    pub fn trigger_run(&self, game: &mut GameState, host_card: CardId) {
902        if self.get_overriding_ability().is_some() {
903            game.card_mut(host_card)
904                .add_ability_activated_for(self.get_overriding_ability());
905        }
906    }
907
908    /// Ensures trigger execute ability can be built from host SVar.
909    pub fn ensure_ability(
910        &self,
911        game: &GameState,
912        host_card: CardId,
913        activating_player: PlayerId,
914    ) -> Option<SpellAbility> {
915        if let Some(overriding_ability) = self.get_overriding_ability() {
916            return Some(overriding_ability.clone());
917        }
918        let holder: &dyn HasSVars = if self.is_intrinsic() {
919            &self.base.card_trait_base
920        } else {
921            game.card(host_card)
922        };
923        if self.execute.is_empty() {
924            return None;
925        }
926        let ability_text = holder
927            .get_svar(&self.execute)
928            .or_else(|| game.card(host_card).get_svar(&self.execute))?;
929        Some(build_spell_ability(
930            game,
931            host_card,
932            ability_text,
933            activating_player,
934        ))
935    }
936
937    pub fn ensure_ability_mut(
938        &mut self,
939        game: &GameState,
940        host_card: CardId,
941        activating_player: PlayerId,
942    ) -> Option<&mut SpellAbility> {
943        if self.get_overriding_ability().is_none() {
944            let ability = self.ensure_ability(game, host_card, activating_player)?;
945            self.set_overriding_ability(ability);
946        }
947        self.base.overriding_ability.as_mut()
948    }
949
950    pub fn is_chapter(&self) -> bool {
951        self.ir.chapter.is_some()
952    }
953
954    pub fn get_chapter(&self) -> Option<i32> {
955        self.ir.chapter
956    }
957
958    pub fn is_last_chapter(&self) -> bool {
959        false
960    }
961
962    pub fn while_keyword_check(&self, _param: &str, _run_params: &RunParams) -> bool {
963        // TODO: JacoRefactor
964        false
965    }
966
967    pub fn set_triggering_objects(
968        &self,
969        sa: &mut SpellAbility,
970        params: &RunParams,
971        game: &GameState,
972        _host_card: CardId,
973        _host_controller: PlayerId,
974    ) {
975        params.add_common_trigger_objects(sa);
976        self.mode.set_triggering_objects(self, sa, params, game);
977    }
978
979    pub fn build_triggered_spell_ability(
980        &self,
981        game: &GameState,
982        host_card: CardId,
983        host_controller: PlayerId,
984        trigger_index: usize,
985        params: &RunParams,
986    ) -> SpellAbility {
987        let host = game.card(host_card);
988        let (mut sa, svar_text) = if let Some(overriding_ability) = self.get_overriding_ability() {
989            (overriding_ability.clone(), String::new())
990        } else {
991            let svar_text = host.get_s_var(&self.execute).map(str::to_string).unwrap_or_else(|| {
992                panic!(
993                    "Trigger::build_triggered_spell_ability missing/empty Execute SVar: host={} execute={} trigger_index={} description={}",
994                    host.card_name,
995                    self.execute,
996                    trigger_index,
997                    self.description
998                )
999            });
1000            (
1001                build_spell_ability(game, host_card, &svar_text, host_controller),
1002                svar_text,
1003            )
1004        };
1005        sa.is_trigger = true;
1006        sa.trigger_source = Some(host_card);
1007        sa.trigger_source_zone_timestamp = Some(game.card(host_card).zone_timestamp);
1008        sa.source_trigger_id = Some(self.id);
1009        sa.trigger_index = Some(trigger_index);
1010        sa.trigger_remembered = self.trigger_remembered.clone();
1011        self.set_triggering_objects(&mut sa, params, game, host_card, host_controller);
1012        self.configure_triggered_spell_ability(&mut sa, params, game, &svar_text);
1013        sa
1014    }
1015
1016    fn configure_triggered_spell_ability(
1017        &self,
1018        sa: &mut SpellAbility,
1019        params: &RunParams,
1020        game: &GameState,
1021        svar_text: &str,
1022    ) {
1023        if let Some(pid) = params.damage_target_player {
1024            sa.target_chosen.target_player = Some(pid);
1025        }
1026        if sa.target_chosen.target_player.is_none() && svar_text.contains("TriggeredPlayer") {
1027            if let Some(pid) = params.player {
1028                sa.target_chosen.target_player = Some(pid);
1029            }
1030        }
1031        if let Some(pid) = params.defending_player {
1032            if sa.target_chosen.target_player.is_none() && svar_text.contains("DefendingPlayer") {
1033                sa.target_chosen.target_player = Some(pid);
1034            }
1035        }
1036        if let Some(cid) = params.damage_target_card {
1037            sa.target_chosen.target_card = Some(cid);
1038        }
1039        if let Some(cause_cid) = params.cause_card {
1040            if let Some(entry) = game.stack.find_by_source_card(cause_cid) {
1041                sa.target_chosen.target_stack_entry = Some(entry.id);
1042            }
1043        }
1044        if let Some(attacker_id) = params.attacker {
1045            if svar_text.contains("TriggeredAttacker") {
1046                sa.target_chosen.target_card = Some(attacker_id);
1047            }
1048        }
1049        if let Some(blocker_id) = params.blocker {
1050            if svar_text.contains("TriggeredBlocker") {
1051                sa.target_chosen.target_card = Some(blocker_id);
1052            }
1053        }
1054        if let Some(lki_p1p1) = params.lki_p1p1_counters {
1055            if self.execute.contains("Modular") || svar_text.contains("Modular") {
1056                sa.trigger_remembered_amount = lki_p1p1;
1057            }
1058        }
1059    }
1060}
1061
1062/// Mirrors Java's TriggerHandler.parseTrigger().
1063/// Parses raw "Mode$ ChangesZone | Origin$ Any | ..." into Trigger struct.
1064pub fn parse_trigger(raw: &str, next_id: &mut u32) -> Option<Trigger> {
1065    let _perf_scope =
1066        crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Trigger);
1067    let params = Params::from_raw(raw);
1068
1069    let mode_str = params.get(keys::MODE)?;
1070    let mode: Box<dyn TriggerBehavior> = match mode_str {
1071        "ChangesZone" => crate::trigger::trigger_changes_zone::TriggerChangesZone::parse(&params),
1072        "Phase" => crate::trigger::trigger_phase::TriggerPhase::parse(&params),
1073        "SpellCast" | "AbilityCast" | "SpellAbilityCast" => {
1074            crate::trigger::trigger_spell_ability_cast_or_copy::TriggerSpellAbilityCastOrCopy::parse(mode_str, &params)
1075        }
1076        "Attacks" => crate::trigger::trigger_attacks::TriggerAttacks::parse(&params),
1077        "Fight" => crate::trigger::trigger_fight::TriggerFight::parse(&params),
1078        "FightOnce" => crate::trigger::trigger_fight_once::TriggerFightOnce::parse(&params),
1079        "DamageDone" => crate::trigger::trigger_damage_done::TriggerDamageDone::parse(&params),
1080        "Countered" => {
1081            let valid_card = params.selector_cloned(keys::VALID_CARD);
1082            let valid_cause = params.selector_cloned(keys::VALID_CAUSE);
1083            let valid_sa = params.get_cloned(keys::VALID_SA);
1084            crate::trigger::trigger_countered::TriggerCountered::parse(valid_card, valid_cause, valid_sa)
1085        }
1086        "Blocks" => crate::trigger::trigger_blocks::TriggerBlocks::parse(&params),
1087        "AttackerBlocked" => crate::trigger::trigger_attacker_blocked::TriggerAttackerBlocked::parse(&params),
1088        "AttackerUnblocked" => crate::trigger::trigger_attacker_unblocked::TriggerAttackerUnblocked::parse(&params),
1089        "LifeGained" => crate::trigger::trigger_life_gained::TriggerLifeGained::parse(&params),
1090        "LifeLost" => crate::trigger::trigger_life_lost::TriggerLifeLost::parse(&params),
1091        "CounterAdded" => crate::trigger::trigger_counter_added::TriggerCounterAdded::parse(&params),
1092        "CounterRemoved" => crate::trigger::trigger_counter_removed::TriggerCounterRemoved::parse(&params),
1093        "Sacrificed" => crate::trigger::trigger_sacrificed::TriggerSacrificed::parse(&params),
1094        "Drawn" => crate::trigger::trigger_drawn::TriggerDrawn::parse(&params),
1095        "Milled" => crate::trigger::trigger_milled::TriggerMilled::parse(&params),
1096        "Taps" => crate::trigger::trigger_taps::TriggerTaps::parse(&params),
1097        "Untaps" => crate::trigger::trigger_untaps::TriggerUntaps::parse(&params),
1098        "Transformed" => crate::trigger::trigger_transformed::TriggerTransformed::parse(&params),
1099        "TurnFaceUp" => crate::trigger::trigger_turn_face_up::TriggerTurnFaceUp::parse(&params),
1100        "Attached" => crate::trigger::trigger_attached::TriggerAttached::parse(&params),
1101        "Unattached" => crate::trigger::trigger_unattach::TriggerUnattach::parse(&params),
1102        "LandPlayed" => crate::trigger::trigger_land_played::TriggerLandPlayed::parse(&params),
1103        "BecomesTarget" => crate::trigger::trigger_becomes_target::TriggerBecomesTarget::parse(&params),
1104        "TapsForMana" => crate::trigger::trigger_taps_for_mana::TriggerTapsForMana::parse(&params),
1105        "AbilityActivated" => crate::trigger::trigger_ability_activated::TriggerAbilityActivated::parse(&params),
1106        "Explored" | "Explores" => crate::trigger::trigger_explores::TriggerExplores::parse(&params),
1107        "BecomeMonstrous" => crate::trigger::trigger_become_monstrous::TriggerBecomeMonstrous::parse(&params),
1108        "BecomeMonarch" => crate::trigger::trigger_become_monarch::TriggerBecomeMonarch::parse(&params),
1109        "DamageDealtOnce" => crate::trigger::trigger_damage_dealt_once::TriggerDamageDealtOnce::parse(&params),
1110        "Destroyed" => crate::trigger::trigger_destroyed::TriggerDestroyed::parse(&params),
1111        "Exiled" => crate::trigger::trigger_exiled::TriggerExiled::parse(&params),
1112        "CollectEvidence" => {
1113            let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1114            crate::trigger::trigger_collect_evidence::TriggerCollectEvidence::parse(valid_player)
1115        }
1116        "Forage" => crate::trigger::trigger_forage::TriggerForage::parse(&params),
1117        "Enlisted" => crate::trigger::trigger_enlisted::TriggerEnlisted::parse(&params),
1118        "FlippedCoin" => crate::trigger::trigger_flipped_coin::TriggerFlippedCoin::parse(&params),
1119        "RolledDie" => crate::trigger::trigger_rolled_die::TriggerRolledDie::parse(&params),
1120        "RolledDieOnce" => crate::trigger::trigger_rolled_die_once::TriggerRolledDieOnce::parse(&params),
1121        "TokenCreated" => crate::trigger::trigger_token_created::TriggerTokenCreated::parse(&params),
1122        "SpellCastOrCopy" | "SpellCopied" | "SpellAbilityCopy" | "SpellCopy" => {
1123            crate::trigger::trigger_spell_ability_cast_or_copy::TriggerSpellAbilityCastOrCopy::parse(mode_str, &params)
1124        }
1125        "AttackersDeclared" | "AttackersDeclaredOneTarget" => {
1126            crate::trigger::trigger_attackers_declared::TriggerAttackersDeclared::parse(mode_str, &params)
1127        }
1128        "BlockersDeclared" => crate::trigger::trigger_blockers_declared::TriggerBlockersDeclared::parse(&params),
1129        "ChangesZoneAll" => crate::trigger::trigger_changes_zone_all::TriggerChangesZoneAll::parse(&params),
1130        "ChangesController" => {
1131            let valid_card = params.selector_cloned(keys::VALID_CARD);
1132            crate::trigger::trigger_changes_controller::TriggerChangesController::parse(valid_card)
1133        }
1134        "TurnBegin" | "NewTurn" => crate::trigger::trigger_turn_begin::TriggerTurnBegin::parse(&params),
1135        "DamageDoneOnce" => crate::trigger::trigger_damage_done_once::TriggerDamageDoneOnce::parse(&params),
1136        "DamageDoneOnceByController" => crate::trigger::trigger_damage_done_once_by_controller::TriggerDamageDoneOnceByController::parse(&params),
1137        "SpellCastAll" => {
1138            crate::trigger::trigger_spell_ability_cast_or_copy::TriggerSpellAbilityCastOrCopy::parse(mode_str, &params)
1139        }
1140        "LifeLostAll" => crate::trigger::trigger_life_lost_all::TriggerLifeLostAll::parse(&params),
1141        "CounterAddedOnce" => {
1142            let valid_card = params.selector_cloned(keys::VALID_CARD);
1143            let counter_type = params.get_cloned(keys::COUNTER_TYPE);
1144            let valid_source = params.selector_cloned(keys::VALID_SOURCE);
1145            crate::trigger::trigger_counter_added_once::TriggerCounterAddedOnce::parse(valid_card, counter_type, valid_source)
1146        }
1147        "CounterAddedAll" => {
1148            let counter_type = params.get_cloned(keys::COUNTER_TYPE);
1149            let valid = params.selector_cloned_any(&[keys::VALID, keys::VALID_CARD]);
1150            crate::trigger::trigger_counter_added_all::TriggerCounterAddedAll::parse(counter_type, valid)
1151        }
1152        "CounterPlayerAddedAll" => {
1153            let valid_source = params.selector_cloned("ValidSource");
1154            let valid_object = params.selector_cloned("ValidObject");
1155            let valid_object_to_source = params.selector_cloned("ValidObjectToSource");
1156            crate::trigger::trigger_counter_player_added_all::TriggerCounterPlayerAddedAll::parse(valid_source, valid_object, valid_object_to_source)
1157        }
1158        "CounterTypeAddedAll" => {
1159            let valid_object = params.selector_cloned("ValidObject");
1160            let first_time_only = params.has("FirstTime");
1161            crate::trigger::trigger_counter_type_added_all::TriggerCounterTypeAddedAll::parse(valid_object, first_time_only)
1162        }
1163        "DiscardedAll" => crate::trigger::trigger_discarded_all::TriggerDiscardedAll::parse(&params),
1164        "Discarded" => crate::trigger::trigger_discarded::TriggerDiscarded::parse(&params),
1165        "SacrificedOnce" => crate::trigger::trigger_sacrificed_once::TriggerSacrificedOnce::parse(&params),
1166        "Cycled" | "Cycling" => crate::trigger::trigger_cycled::TriggerCycled::parse(&params),
1167        "PhasedIn" | "PhaseIn" => crate::trigger::trigger_phase_in::TriggerPhaseIn::parse(&params),
1168        "PhasedOut" | "PhaseOut" => crate::trigger::trigger_phase_out::TriggerPhaseOut::parse(&params),
1169        "Always" => crate::trigger::trigger_always::TriggerAlways::parse(&params),
1170        "Immediate" => crate::trigger::trigger_immediate::TriggerImmediate::parse(&params),
1171        "Surveil" => crate::trigger::trigger_surveil::TriggerSurveil::parse(&params),
1172        "Scry" => crate::trigger::trigger_scry::TriggerScry::parse(&params),
1173        "Foretell" | "Foretold" => crate::trigger::trigger_foretell::TriggerForetell::parse(&params),
1174        "SearchedLibrary" => crate::trigger::trigger_searched_library::TriggerSearchedLibrary::parse(&params),
1175        "Shuffled" => crate::trigger::trigger_shuffled::TriggerShuffled::parse(&params),
1176        "ManaAdded" => crate::trigger::trigger_mana_added::TriggerManaAdded::parse(&params),
1177        "TokenCreatedOnce" => crate::trigger::trigger_token_created_once::TriggerTokenCreatedOnce::parse(&params),
1178        "TapAll" => crate::trigger::trigger_tap_all::TriggerTapAll::parse(&params),
1179        "UntapAll" => crate::trigger::trigger_untap_all::TriggerUntapAll::parse(&params),
1180        "BecomesTargetOnce" => crate::trigger::trigger_becomes_target_once::TriggerBecomesTargetOnce::parse(&params),
1181        "AttackerBlockedByCreature" => crate::trigger::trigger_attacker_blocked_by_creature::TriggerAttackerBlockedByCreature::parse(&params),
1182        "AttackerBlockedOnce" => crate::trigger::trigger_attacker_blocked_once::TriggerAttackerBlockedOnce::parse(&params),
1183        "AttackerUnblockedOnce" => crate::trigger::trigger_attacker_unblocked_once::TriggerAttackerUnblockedOnce::parse(&params),
1184        "SpellCastOnce" | "SpellCastOfType" => {
1185            crate::trigger::trigger_spell_ability_cast_or_copy::TriggerSpellAbilityCastOrCopy::parse(mode_str, &params)
1186        }
1187        "DamageAll" => crate::trigger::trigger_damage_all::TriggerDamageAll::parse(&params),
1188        "DamagePreventedOnce" => crate::trigger::trigger_damage_prevented_once::TriggerDamagePreventedOnce::parse(&params),
1189        "ExcessDamage" => crate::trigger::trigger_excess_damage::TriggerExcessDamage::parse(&params),
1190        "ExcessDamageAll" => crate::trigger::trigger_excess_damage_all::TriggerExcessDamageAll::parse(&params),
1191        "CounterRemovedOnce" => {
1192            let valid_card = params.selector_cloned(keys::VALID_CARD);
1193            let counter_type = params.get_cloned(keys::COUNTER_TYPE);
1194            crate::trigger::trigger_counter_removed_once::TriggerCounterRemovedOnce::parse(valid_card, counter_type)
1195        }
1196        "Exerted" => crate::trigger::trigger_exerted::TriggerExerted::parse(&params),
1197        "ManaExpend" => crate::trigger::trigger_mana_expend::TriggerManaExpend::parse(&params),
1198        "Mutates" => crate::trigger::trigger_mutates::TriggerMutates::parse(&params),
1199        "SetInMotion" => crate::trigger::trigger_set_in_motion::TriggerSetInMotion::parse(&params),
1200        "CaseSolved" => {
1201            let valid_card = params.selector_cloned(keys::VALID_CARD);
1202            let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1203            crate::trigger::trigger_case_solved::TriggerCaseSolved::parse(valid_card, valid_player)
1204        }
1205        "ClaimPrize" => {
1206            let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1207            let valid_card = params.selector_cloned(keys::VALID_CARD);
1208            crate::trigger::trigger_claim_prize::TriggerClaimPrize::parse(valid_player, valid_card)
1209        }
1210        "TakesInitiative" | "TakeInitiative" => crate::trigger::trigger_takes_initiative::TriggerTakesInitiative::parse(&params),
1211        "Adapt" => crate::trigger::trigger_adapt::TriggerAdapt::parse(&params),
1212        "BecomeRenowned" => crate::trigger::trigger_become_renowned::TriggerBecomeRenowned::parse(&params),
1213        "Evolved" => crate::trigger::trigger_evolved::TriggerEvolved::parse(&params),
1214        "BecomesPlotted" => crate::trigger::trigger_becomes_plotted::TriggerBecomesPlotted::parse(&params),
1215        "Investigated" => crate::trigger::trigger_investigated::TriggerInvestigated::parse(&params),
1216        "Proliferate" => crate::trigger::trigger_proliferate::TriggerProliferate::parse(&params),
1217        "CompletedDungeon" | "DungeonCompleted" => {
1218            let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1219            crate::trigger::trigger_completed_dungeon::TriggerCompletedDungeon::parse(valid_player)
1220        }
1221        "CommitCrime" => {
1222            let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1223            crate::trigger::trigger_commit_crime::TriggerCommitCrime::parse(valid_player)
1224        }
1225        "GiveGift" => crate::trigger::trigger_give_gift::TriggerGiveGift::parse(&params),
1226        "RingTemptsYou" => crate::trigger::trigger_ring_tempts_you::TriggerRingTemptsYou::parse(&params),
1227        "PayLife" => crate::trigger::trigger_pay_life::TriggerPayLife::parse(&params),
1228        "PayEcho" => crate::trigger::trigger_pay_echo::TriggerPayEcho::parse(&params),
1229        "ClassLevelGained" => {
1230            let valid_card = params.selector_cloned(keys::VALID_CARD);
1231            let class_level = params.as_i32("ClassLevel");
1232            crate::trigger::trigger_class_level_gained::TriggerClassLevelGained::parse(valid_card, class_level)
1233        }
1234        "NewGame" => crate::trigger::trigger_new_game::TriggerNewGame::parse(&params),
1235        "DayTimeChanges" => crate::trigger::trigger_day_time_changes::TriggerDayTimeChanges::parse(&params),
1236        "LosesGame" => crate::trigger::trigger_loses_game::TriggerLosesGame::parse(&params),
1237        "Discover" => crate::trigger::trigger_discover::TriggerDiscover::parse(&params),
1238        "Elementalbend" | "ElementalBend" | "Airbend" | "Earthbend" | "Firebend" | "Waterbend" => {
1239            crate::trigger::trigger_elementalbend::TriggerElementalbend::parse(&params)
1240        }
1241        "PlanarDice" => crate::trigger::trigger_planar_dice::TriggerPlanarDice::parse(&params),
1242        "PhaseOutAll" => crate::trigger::trigger_phase_out_all::TriggerPhaseOutAll::parse(&params),
1243        "VisitAttraction" => crate::trigger::trigger_visit_attraction::TriggerVisitAttraction::parse(&params),
1244        "EnteredRoom" | "RoomEntered" => crate::trigger::trigger_entered_room::TriggerEnteredRoom::parse(&params),
1245        "MilledAll" => crate::trigger::trigger_milled_all::TriggerMilledAll::parse(&params),
1246        "MilledOnce" => crate::trigger::trigger_milled_once::TriggerMilledOnce::parse(&params),
1247        "Abandoned" => crate::trigger::trigger_abandoned::TriggerAbandoned::parse(&params),
1248        "ManifestDread" => crate::trigger::trigger_manifest_dread::TriggerManifestDread::parse(&params),
1249        "Specializes" => crate::trigger::trigger_specializes::TriggerSpecializes::parse(&params),
1250        "Trains" => crate::trigger::trigger_trains::TriggerTrains::parse(&params),
1251        "Devoured" => crate::trigger::trigger_devoured::TriggerDevoured::parse(&params),
1252        "ConjureAll" => {
1253            let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1254            let valid_card = params.selector_cloned(keys::VALID_CARD);
1255            crate::trigger::trigger_conjure_all::TriggerConjureAll::parse(valid_player, valid_card)
1256        }
1257        "SeekAll" => crate::trigger::trigger_seek_all::TriggerSeekAll::parse(&params),
1258        "BecomesCrewed" => crate::trigger::trigger_becomes_crewed::TriggerBecomesCrewed::parse(&params),
1259        "Championed" => {
1260            let valid_card = params.selector_cloned_any(&["ValidCard", "ValidChampioned"]);
1261            let valid_source = params.selector_cloned("ValidSource");
1262            crate::trigger::trigger_championed::TriggerChampioned::parse(valid_card, valid_source)
1263        }
1264        "Clashed" => {
1265            let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1266            let won = params.get("Won").map(|v| v.eq_ignore_ascii_case("True"));
1267            crate::trigger::trigger_clashed::TriggerClashed::parse(valid_player, won)
1268        }
1269        "Mentored" => crate::trigger::trigger_mentored::TriggerMentored::parse(&params),
1270        "FullyUnlock" => crate::trigger::trigger_fully_unlock::TriggerFullyUnlock::parse(&params),
1271        "AbilityResolves" => crate::trigger::trigger_ability_resolves::TriggerAbilityResolves::parse(&params),
1272        "AbilityTriggered" => crate::trigger::trigger_ability_triggered::TriggerAbilityTriggered::parse(&params),
1273        "UnlockDoor" => crate::trigger::trigger_unlock_door::TriggerUnlockDoor::parse(&params),
1274        "Vote" => crate::trigger::trigger_vote::TriggerVote::parse(&params),
1275        "PlaneswalkedFrom" => crate::trigger::trigger_planeswalked_from::TriggerPlaneswalkedFrom::parse(&params),
1276        "PlaneswalkedTo" | "Planeswalk" => crate::trigger::trigger_planeswalked_to::TriggerPlaneswalkedTo::parse(&params),
1277        "CrankContraption" | "CrankAdvanced" => crate::trigger::trigger_crank_contraption::TriggerCrankContraption::parse(&params),
1278        "PayCumulativeUpkeep" => crate::trigger::trigger_pay_cumulative_upkeep::TriggerPayCumulativeUpkeep::parse(&params),
1279        "ChaosEnsues" => {
1280            let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1281            crate::trigger::trigger_chaos_ensues::TriggerChaosEnsues::parse(valid_player)
1282        }
1283        "BecomesSaddled" => crate::trigger::trigger_becomes_saddled::TriggerBecomesSaddled::parse(&params),
1284        "Crewed" | "Saddled" | "Stationed" => crate::trigger::trigger_crewed_saddled::TriggerCrewedSaddled::parse(&params),
1285        "Unattach" => crate::trigger::trigger_unattach::TriggerUnattach::parse(&params),
1286        "Exploited" => crate::trigger::trigger_exploited::TriggerExploited::parse(&params),
1287        _ => return None,
1288    };
1289
1290    let optional = params.has(keys::OPTIONAL_DECIDER);
1291    let static_trigger = params.has("Static");
1292
1293    // Parse active zones (default: Battlefield)
1294    let mut active_zones = params
1295        .get(keys::TRIGGER_ZONES)
1296        .map(|s| {
1297            s.split(',')
1298                .filter_map(|z| ZoneType::from_str_compat(z.trim()))
1299                .collect::<Vec<_>>()
1300        })
1301        .unwrap_or_else(|| vec![ZoneType::Battlefield]);
1302    // SpellCast triggers with ValidCard$ Card.Self fire while the card is on
1303    // the stack (cast triggers). Java handles these via addTrigAbility() on
1304    // the stack entry. In Rust, add Stack to active zones so the trigger is
1305    // registered when the card is being cast.
1306    if matches!(
1307        mode.trigger_type(),
1308        TriggerType::SpellCast | TriggerType::SpellCastOrCopy
1309    ) {
1310        let valid_card_is_self = params
1311            .selector(keys::VALID_CARD)
1312            .map(|selector| selector.is_any_of(["Card.Self"]))
1313            .unwrap_or(false);
1314        if valid_card_is_self && !active_zones.contains(&ZoneType::Stack) {
1315            active_zones.push(ZoneType::Stack);
1316        }
1317    }
1318
1319    if !params.has(keys::TRIGGER_ZONES) && mode.trigger_type() == TriggerType::ChangesZone {
1320        if let Some(valid_card) = params.selector(keys::VALID_CARD) {
1321            let leaves_battlefield = params
1322                .get(keys::ORIGIN)
1323                .map(|origin| origin.split(',').any(|z| z.trim() == "Battlefield"))
1324                .unwrap_or(false);
1325            if leaves_battlefield && !static_trigger {
1326                active_zones = vec![ZoneType::Battlefield];
1327            }
1328
1329            let self_trigger = valid_card
1330                .alternatives
1331                .iter()
1332                .flat_map(|alternative| &alternative.parts)
1333                .any(|part| part.value.eq_ignore_ascii_case("Self"));
1334            let any_origin = params
1335                .get(keys::ORIGIN)
1336                .map(|origin| origin.eq_ignore_ascii_case("Any"))
1337                .unwrap_or(true);
1338            if self_trigger && any_origin {
1339                if let Some(destinations) = params.get(keys::DESTINATION) {
1340                    let zones = destinations
1341                        .split(',')
1342                        .filter_map(|z| ZoneType::from_str_compat(z.trim()))
1343                        .collect::<Vec<_>>();
1344                    if !zones.is_empty() {
1345                        active_zones = zones;
1346                    }
1347                }
1348            }
1349        }
1350    }
1351
1352    let ir = TriggerIr::from_params(&params);
1353    let execute = params.get_cloned(keys::EXECUTE).unwrap_or_default();
1354    let description = params
1355        .get_cloned(keys::TRIGGER_DESCRIPTION)
1356        .unwrap_or_default();
1357
1358    let id = *next_id;
1359    *next_id += 1;
1360
1361    let mut base = TriggerReplacementBase::default();
1362    base.card_trait_base.set_id(id as i32);
1363    base.card_trait_base.set_intrinsic(true);
1364    base.valid_host_zones = Some(active_zones);
1365
1366    let kind = mode.trigger_type();
1367    Some(Trigger {
1368        id,
1369        base,
1370        kind,
1371        mode,
1372        ir,
1373        execute,
1374        optional,
1375        description,
1376        static_trigger,
1377        trigger_remembered: Vec::new(),
1378        spawning_ability: None,
1379    })
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384    use super::*;
1385    use crate::card::Card;
1386    use crate::event::RunParams;
1387    use crate::ids::{CardId, PlayerId};
1388    use crate::spellability::SpellAbility;
1389    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
1390
1391    #[test]
1392    fn parse_pipe_params_basic() {
1393        let params = Params::from_raw("Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDraw");
1394        assert_eq!(params.get("Mode"), Some("ChangesZone"));
1395        assert_eq!(params.get("Origin"), Some("Any"));
1396        assert_eq!(params.get("Destination"), Some("Battlefield"));
1397        assert_eq!(params.selector_value("ValidCard"), Some("Card.Self"));
1398        assert_eq!(params.get("Execute"), Some("TrigDraw"));
1399    }
1400
1401    #[test]
1402    fn parse_trigger_changes_zone() {
1403        let mut next_id = 0;
1404        let trigger = parse_trigger(
1405            "Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDraw | TriggerDescription$ When CARDNAME enters the battlefield, draw two cards.",
1406            &mut next_id,
1407        ).unwrap();
1408
1409        assert_eq!(trigger.id, 0);
1410        assert_eq!(trigger.execute, "TrigDraw");
1411        assert_eq!(trigger.kind, TriggerType::ChangesZone);
1412    }
1413}