Skip to main content

manabrew_engine/trigger/
trigger_handler.rs

1use std::collections::HashSet;
2
3use forge_foundation::ZoneType;
4
5use crate::agent::{notify_all_agents, GameLogEvent, PlayerAgent};
6use crate::card::valid_filter;
7use crate::event::RunParams;
8use crate::game::GameState;
9use crate::ids::{CardId, PlayerId};
10use crate::mana::ManaPool;
11use crate::parsing::compare::compare_expr;
12use crate::spellability::{build_spell_ability, StackEntry};
13use crate::trigger::TriggerType;
14use crate::trigger::{parse_trigger, Trigger};
15
16/// An active trigger reference — (card_id, trigger_index) pair.
17/// In Java this is a direct Trigger object reference. In Rust,
18/// we use IDs because of the arena pattern (no references).
19#[derive(Debug, Clone)]
20struct ActiveTrigger {
21    card_id: CardId,
22    trigger_index: usize,
23}
24
25/// Mirrors Java's TriggerWaiting.
26#[derive(Debug, Clone)]
27struct TriggerWaiting {
28    mode: TriggerType,
29    params: RunParams,
30    trigger_refs: Option<Vec<(CardId, usize)>>,
31}
32
33/// A one-shot delayed trigger.
34#[derive(Debug, Clone)]
35pub struct DelayedTrigger {
36    pub mode: TriggerType,
37    pub trigger_mode: Box<dyn crate::trigger::TriggerBehavior>,
38    pub params: crate::parsing::Params,
39    pub execute_svar: String,
40    pub controller: PlayerId,
41    pub source_card: CardId,
42    /// Turn number when this delayed trigger was registered.
43    pub created_turn: u32,
44    /// Phase during which this delayed trigger was registered.
45    pub created_phase: forge_foundation::PhaseType,
46    /// Optional target card for the delayed trigger (e.g. the creature to bounce for Dash
47    /// or sacrifice for Blitz at end of turn).
48    pub target_card: Option<CardId>,
49    /// Sum of integer values remembered by the delayed trigger at creation.
50    pub remembered_amount: i32,
51    /// Cards remembered by the delayed trigger (e.g. `RememberObjects$ Remembered`
52    /// copies the source card's remembered_cards snapshot at registration time).
53    /// Exposed to the executed ability via `SpellAbility::trigger_remembered`.
54    #[allow(dead_code)]
55    pub remembered_cards: Vec<CardId>,
56    /// Players remembered by the delayed trigger (e.g.
57    /// `RememberObjects$ RememberedController` snapshots the controllers of
58    /// the source's remembered cards at registration time, used by Arcane
59    /// Denial's "may draw up to two cards" trigger to remember the controller
60    /// of the countered spell). Exposed via `SpellAbility::trigger_remembered`
61    /// as `AbilityValue::Player(_)` entries.
62    pub remembered_players: Vec<PlayerId>,
63    /// Snapshot of remembered cards for delayed-trigger `RememberedLKI` lookups.
64    pub remembered_lki_cards: Vec<CardId>,
65    /// When true, this delayed trigger should sort AFTER same-event active
66    /// triggers (resolve FIRST on the LIFO stack). Used to mirror Java's
67    /// trigger-ID-based ordering for granted-evoke sacrifice triggers, whose
68    /// trigger ID is assigned after the card's intrinsic T: triggers and thus
69    /// lands on top of the stack.
70    pub sort_after_active: bool,
71    /// Java delayed triggers are real Trigger instances and receive a trigger
72    /// id when parsed. Assign the equivalent order at registration so
73    /// simultaneous delayed triggers do not tie on host timestamp.
74    pub trigger_order: Option<u32>,
75}
76
77impl DelayedTrigger {
78    /// Build a temporary `Trigger` wrapper for calling `TriggerBehavior` trait methods
79    /// that require a `&Trigger` reference (Java's `this`).
80    pub fn as_trigger(&self, _game: &crate::game::GameState) -> crate::trigger::Trigger {
81        let Self {
82            params: delayed_params,
83            ..
84        } = self;
85        let mut base =
86            crate::game_loop::trigger_replacement_base::TriggerReplacementBase::default();
87        base.set_host_card_id(self.source_card);
88        crate::trigger::Trigger {
89            id: self.trigger_order.unwrap_or(u32::MAX),
90            base,
91            kind: self.mode,
92            mode: dyn_clone::clone_box(&*self.trigger_mode),
93            ir: crate::trigger::trigger_ir::TriggerIr::from_params(delayed_params),
94            execute: self.execute_svar.clone(),
95            optional: false,
96            description: String::new(),
97            static_trigger: false,
98            trigger_remembered: self
99                .remembered_cards
100                .iter()
101                .copied()
102                .map(crate::event::AbilityValue::Card)
103                .collect(),
104            spawning_ability: None,
105        }
106    }
107}
108
109/// A triggered ability ready to be placed on the stack, with optional metadata.
110#[derive(Debug, Clone)]
111pub struct PendingTrigger {
112    pub entry: StackEntry,
113    pub ability_triggered: Option<RunParams>,
114    /// Whether this trigger is optional (has OptionalDecider$).
115    pub optional: bool,
116    /// The player who decides whether the trigger fires (usually the controller).
117    pub decider: PlayerId,
118    /// Description text for the trigger (shown to player for optional triggers).
119    pub description: String,
120}
121
122#[derive(Debug, Clone)]
123pub struct TriggerPushLog {
124    pub source_name: String,
125    pub player_name: String,
126    pub optional: bool,
127    pub trigger_api: String,
128}
129
130type MatchedTrigger = (PendingTrigger, PlayerId, u64, u8, u32);
131
132/// Mirrors Java's TriggerHandler — central trigger dispatcher.
133/// In Java, lives on Game. In Rust, lives on GameLoop because
134/// active_triggers and waiting_triggers are transient processing state.
135#[allow(dead_code)]
136#[derive(Debug, Clone)]
137pub struct TriggerHandler {
138    active_triggers: Vec<ActiveTrigger>,
139    waiting_triggers: Vec<TriggerWaiting>,
140    delayed_triggers: Vec<DelayedTrigger>,
141    this_turn_delayed_triggers: Vec<DelayedTrigger>,
142    player_defined_delayed_triggers: Vec<(PlayerId, DelayedTrigger)>,
143    suppressed_modes: HashSet<TriggerType>,
144    all_suppressed: bool,
145    next_trigger_id: u32,
146    /// Triggers that were matched early (before SBA) and are waiting to be
147    /// placed on the stack. This ensures triggers from creatures that die to
148    /// SBA (e.g. Raptor Hatchling's enrage) are not lost.
149    /// Tuple: (PendingTrigger, controller, zone timestamp, trigger bucket, trigger order).
150    pre_matched_triggers: Vec<MatchedTrigger>,
151}
152
153impl TriggerHandler {
154    pub fn new() -> Self {
155        TriggerHandler {
156            active_triggers: Vec::new(),
157            waiting_triggers: Vec::new(),
158            delayed_triggers: Vec::new(),
159            this_turn_delayed_triggers: Vec::new(),
160            player_defined_delayed_triggers: Vec::new(),
161            suppressed_modes: HashSet::new(),
162            all_suppressed: false,
163            next_trigger_id: 0,
164            pre_matched_triggers: Vec::new(),
165        }
166    }
167
168    /// Mirrors Java's runTrigger() — main entry point.
169    /// Called from game actions when events occur.
170    /// If `hold` is true, event is queued; otherwise it's also queued
171    /// (all triggers go through the waiting queue for APNAP ordering).
172    pub fn run_trigger(&mut self, mode: TriggerType, params: RunParams, hold: bool) {
173        if self.is_trigger_suppressed(mode) {
174            return;
175        }
176
177        // Java parity: mana triggers are not held/frozen. We don't run them inline
178        // here because matching still flows through waiting processing, but we do
179        // force front-of-queue delivery when hold is false.
180        let urgent = !hold
181            && matches!(
182                mode,
183                TriggerType::Always | TriggerType::TapsForMana | TriggerType::ManaAdded
184            );
185        if urgent {
186            self.waiting_triggers
187                .insert(0, self.build_waiting_trigger(mode, params.clone()));
188        } else {
189            self.waiting_triggers
190                .push(self.build_waiting_trigger(mode, params.clone()));
191        }
192
193        if mode == TriggerType::SpellCast {
194            self.waiting_triggers
195                .push(self.build_waiting_trigger(TriggerType::SpellAbilityCast, params.clone()));
196            self.waiting_triggers
197                .push(self.build_waiting_trigger(TriggerType::SpellCastOrCopy, params.clone()));
198        }
199        if mode == TriggerType::AbilityCast {
200            self.waiting_triggers
201                .push(self.build_waiting_trigger(TriggerType::SpellAbilityCast, params.clone()));
202        }
203        if mode == TriggerType::SpellCopied {
204            self.waiting_triggers
205                .push(self.build_waiting_trigger(TriggerType::SpellCopy, params.clone()));
206            self.waiting_triggers
207                .push(self.build_waiting_trigger(TriggerType::SpellAbilityCopy, params.clone()));
208            self.waiting_triggers
209                .push(self.build_waiting_trigger(TriggerType::SpellCastOrCopy, params.clone()));
210        }
211    }
212
213    /// Java-parity entrypoint that can resolve triggers immediately when the
214    /// event should not be held/frozen.
215    pub fn run_trigger_with_game(
216        &mut self,
217        game: &GameState,
218        mode: TriggerType,
219        params: RunParams,
220        hold: bool,
221    ) -> Vec<PendingTrigger> {
222        self.run_trigger(mode, params, hold);
223        let resolve_now = mode == TriggerType::Always
224            || (!hold
225                && !game.stack.is_frozen()
226                && !matches!(mode, TriggerType::TapsForMana | TriggerType::ManaAdded));
227        if resolve_now {
228            self.run_waiting_triggers(game)
229        } else {
230            Vec::new()
231        }
232    }
233
234    /// Java parity wrapper for TriggerHandler.parseTrigger(String).
235    pub fn parse_trigger(&mut self, raw: &str) -> Option<Trigger> {
236        parse_trigger(raw, &mut self.next_trigger_id)
237    }
238
239    /// Java parity wrapper for TriggerHandler.collectTriggerForWaiting(...).
240    pub fn collect_trigger_for_waiting(&mut self, mode: TriggerType, params: RunParams) {
241        self.waiting_triggers
242            .push(self.build_waiting_trigger(mode, params));
243    }
244
245    /// Number of triggers in the waiting queue (for debug/diagnostics).
246    pub fn waiting_trigger_count(&self) -> usize {
247        self.waiting_triggers.len()
248    }
249
250    pub fn pre_matched_trigger_count(&self) -> usize {
251        self.pre_matched_triggers.len()
252    }
253
254    /// Match waiting triggers NOW, while source cards are still in their
255    /// current zones.  Stores results in `pre_matched_triggers` so that a
256    /// subsequent `run_waiting_triggers` call returns them even if SBA has
257    /// since moved the source card (e.g. Raptor Hatchling dying to combat
258    /// damage before triggers go on the stack).
259    ///
260    /// Call this immediately after firing triggers and before SBA.
261    /// Returns true if any active trigger is a Drawn trigger with Number$ set.
262    /// Used to gate flush_waiting_triggers during draws to avoid disrupting
263    /// other trigger matching when no Number$-gated Drawn triggers exist.
264    pub fn has_number_drawn_triggers(&self, game: &GameState) -> bool {
265        self.active_triggers.iter().any(|at| {
266            let card = game.card(at.card_id);
267            if at.trigger_index >= card.triggers.len() {
268                return false;
269            }
270            {
271                let t = &card.triggers[at.trigger_index];
272                t.kind == TriggerType::Drawn && t.mode.drawn_number().is_some()
273            }
274        })
275    }
276
277    pub fn flush_waiting_triggers(&mut self, game: &GameState) {
278        if self.waiting_triggers.is_empty() && self.delayed_triggers.is_empty() {
279            return;
280        }
281        let matched = self.match_waiting_triggers(game);
282        self.pre_matched_triggers.extend(matched);
283    }
284
285    /// Mirrors Java's runWaitingTriggers().
286    /// Drains waiting queue, matches triggers, returns PendingTriggers.
287    /// The caller (game_loop) handles OptionalDecider$ prompting.
288    pub fn run_waiting_triggers(&mut self, game: &GameState) -> Vec<PendingTrigger> {
289        // Start with any triggers that were pre-matched (flushed before SBA).
290        let mut entries: Vec<MatchedTrigger> = std::mem::take(&mut self.pre_matched_triggers);
291
292        if self.waiting_triggers.is_empty() && self.delayed_triggers.is_empty() {
293            if entries.is_empty() {
294                return Vec::new();
295            }
296            // Only have pre-matched — apply APNAP + zone timestamp ordering.
297            let active_player = game.active_player();
298            entries.sort_by_key(|(_, controller, ts, trigger_bucket, trigger_order)| {
299                (
300                    if *controller == active_player { 0u8 } else { 1 },
301                    if *trigger_bucket == 2 { 1u8 } else { 0 },
302                    *ts,
303                    *trigger_bucket,
304                    *trigger_order,
305                )
306            });
307            return entries
308                .into_iter()
309                .map(|(pending, _, _, _, _)| pending)
310                .collect();
311        }
312
313        // Match any remaining waiting triggers (those fired after the flush).
314        entries.extend(self.match_waiting_triggers(game));
315
316        // Fire Immediate delayed triggers — these fire "as soon as possible"
317        // without waiting for a matching event (mirrors Java registerDelayedTrigger
318        // with TriggerType.Immediate).
319        entries.extend(self.fire_immediate_delayed_triggers(game));
320
321        // APNAP ordering: active player's triggers first.
322        // Within the same player, order by zone_timestamp (older cards first),
323        // matching Java's forEachCardInGame() which iterates by Zone.cardList
324        // insertion order.
325        let active_player = game.active_player();
326        entries.sort_by_key(|(_, controller, ts, trigger_bucket, trigger_order)| {
327            (
328                if *controller == active_player { 0u8 } else { 1 },
329                if *trigger_bucket == 2 { 1u8 } else { 0 },
330                *ts,
331                *trigger_bucket,
332                *trigger_order,
333            )
334        });
335
336        entries
337            .into_iter()
338            .map(|(pending, _, _, _, _)| pending)
339            .collect()
340    }
341
342    pub fn process_waiting_triggers(
343        &mut self,
344        mana_pools: &[ManaPool],
345        game: &mut GameState,
346        agents: &mut [Box<dyn PlayerAgent>],
347    ) -> Vec<TriggerPushLog> {
348        let pending = self.run_waiting_triggers(game);
349        self.process_pending_triggers(mana_pools, game, agents, pending)
350    }
351
352    pub fn process_pending_triggers(
353        &mut self,
354        mana_pools: &[ManaPool],
355        game: &mut GameState,
356        agents: &mut [Box<dyn PlayerAgent>],
357        pending: Vec<PendingTrigger>,
358    ) -> Vec<TriggerPushLog> {
359        let mut push_logs = Vec::new();
360        for mut pt in pending {
361            let source_name = pt
362                .entry
363                .spell_ability
364                .source
365                .and_then(|id| game.cards.get(id.index()).map(|c| c.card_name.clone()))
366                .unwrap_or_else(|| "Triggered ability".to_string());
367            let player_name = game
368                .player(pt.entry.spell_ability.activating_player)
369                .name
370                .clone();
371
372            if pt.entry.spell_ability.api == Some(crate::ability::api_type::ApiType::Charm)
373                && !crate::ability::effects::charm_effect::make_choices_precast(
374                    game,
375                    agents,
376                    &mut pt.entry.spell_ability,
377                )
378            {
379                continue;
380            }
381
382            let setup_result = pt
383                .entry
384                .spell_ability
385                .setup_targets(game, agents, mana_pools);
386            if !setup_result {
387                continue;
388            }
389            if let Some(source_id) = pt.entry.spell_ability.source {
390                crate::ability::effects::emit_targeting_triggers_for_sa(
391                    self,
392                    game,
393                    source_id,
394                    &pt.entry.spell_ability,
395                );
396            }
397
398            if pt.optional {
399                pt.entry.optional_trigger_decider = Some(pt.decider);
400                pt.entry.optional_trigger_description = Some(pt.description.clone());
401                pt.entry.optional_trigger_source_name = Some(source_name.clone());
402            }
403
404            if let Some(source_id) = pt.entry.spell_ability.trigger_source {
405                if let Some(trig_idx) = pt.entry.spell_ability.trigger_index {
406                    if let Some(trigger) = game
407                        .cards
408                        .get(source_id.index())
409                        .and_then(|c| c.triggers.get(trig_idx))
410                        .cloned()
411                    {
412                        trigger.trigger_run(game, source_id);
413                    }
414                }
415            }
416
417            let trigger_mode = pt
418                .entry
419                .spell_ability
420                .trigger_source
421                .and_then(|source_id| {
422                    pt.entry.spell_ability.trigger_index.and_then(|idx| {
423                        game.cards
424                            .get(source_id.index())
425                            .and_then(|c| c.triggers.get(idx))
426                            .map(|t| t.kind.name().to_string())
427                    })
428                })
429                .unwrap_or_else(|| "DelayedOrUnknown".to_string());
430            let trigger_api = pt
431                .entry
432                .spell_ability
433                .api
434                .map(|a| a.name().to_string())
435                .unwrap_or_else(|| "Unknown".to_string());
436
437            let trigger_msg = if pt.description.is_empty() {
438                format!(
439                    "Trigger fired: mode={} | api={} | source={}",
440                    trigger_mode, trigger_api, source_name
441                )
442            } else {
443                format!(
444                    "Trigger fired: mode={} | api={} | source={} | {}",
445                    trigger_mode, trigger_api, source_name, pt.description
446                )
447            };
448            let mut event = GameLogEvent::stack(trigger_msg)
449                .with_player(pt.entry.spell_ability.activating_player);
450            if let Some(source_id) = pt.entry.spell_ability.source {
451                event = event.with_source_card(source_id);
452            }
453            if let Some(target_id) = pt.entry.spell_ability.target_chosen.target_card {
454                event = event.with_target_card(target_id);
455            }
456            notify_all_agents(agents, event);
457
458            let is_optional = pt.optional;
459            let pushed_entry = pt.entry.clone();
460            game.stack.push(pt.entry);
461            if pushed_entry.spell_ability.is_trigger {
462                let source_card = pushed_entry.spell_ability.source;
463                self.run_trigger(
464                    TriggerType::SpellAbilityCast,
465                    RunParams {
466                        card: source_card,
467                        spell_card: source_card,
468                        player: Some(pushed_entry.spell_ability.activating_player),
469                        activator: Some(pushed_entry.spell_ability.activating_player),
470                        spell_controller: Some(pushed_entry.spell_ability.activating_player),
471                        spell_ability: Some(pushed_entry.spell_ability.clone()),
472                        source_sa: Some(pushed_entry.spell_ability.clone()),
473                        cause: Some(pushed_entry.spell_ability.clone()),
474                        cause_card: source_card,
475                        ..Default::default()
476                    },
477                    true,
478                );
479                if let Some(mut ability_triggered) = pt.ability_triggered.clone() {
480                    ability_triggered.spell_ability = Some(pushed_entry.spell_ability.clone());
481                    ability_triggered.source_sa = Some(pushed_entry.spell_ability.clone());
482                    if ability_triggered.cause_card.is_none() {
483                        ability_triggered.cause_card = source_card;
484                    }
485                    self.run_trigger(TriggerType::AbilityTriggered, ability_triggered, false);
486                }
487            }
488            push_logs.push(TriggerPushLog {
489                source_name,
490                player_name,
491                optional: is_optional,
492                trigger_api,
493            });
494        }
495        push_logs
496    }
497
498    /// Drain `waiting_triggers`, match them against active and delayed triggers,
499    /// and return the matched entries.  This is the core matching logic shared by
500    /// both `flush_waiting_triggers` and `run_waiting_triggers`.
501    fn match_waiting_triggers(&mut self, game: &GameState) -> Vec<MatchedTrigger> {
502        let waiting = std::mem::take(&mut self.waiting_triggers);
503        let mut entries: Vec<MatchedTrigger> = Vec::new();
504
505        for event in &waiting {
506            let mut trigger_refs: Vec<(CardId, usize, usize)> =
507                if let Some(stored_refs) = &event.trigger_refs {
508                    stored_refs
509                        .iter()
510                        .enumerate()
511                        .map(|(idx, (card_id, trigger_index))| (*card_id, *trigger_index, idx))
512                        .collect()
513                } else {
514                    self.active_triggers
515                        .iter()
516                        .enumerate()
517                        .map(|(idx, active)| (active.card_id, active.trigger_index, idx))
518                        .collect()
519                };
520            if event.trigger_refs.is_none() {
521                for (card_id, trigger_index) in self.ltb_trigger_refs_for_event(game, event) {
522                    if !trigger_refs.iter().any(|(existing_card, existing_idx, _)| {
523                        *existing_card == card_id && *existing_idx == trigger_index
524                    }) {
525                        trigger_refs.push((card_id, trigger_index, usize::MAX));
526                    }
527                }
528            }
529            trigger_refs.sort_by_key(|&(card_id, trigger_index, idx)| {
530                let card = game.card(card_id);
531                let is_static = card
532                    .triggers
533                    .get(trigger_index)
534                    .map(|trigger| !trigger.is_static())
535                    .unwrap_or(true);
536                (is_static, idx)
537            });
538
539            let event_entries_start = entries.len();
540
541            // Check each active trigger plus any Java-style LTB look-back triggers.
542            for (card_id, trigger_index, _) in trigger_refs {
543                let card = game.card(card_id);
544                if trigger_index >= card.triggers.len() {
545                    continue;
546                }
547                let trigger = &card.triggers[trigger_index];
548                let host_controller = card.controller;
549                if crate::staticability::static_ability_disable_triggers::is_disabled(
550                    game,
551                    card_id,
552                    trigger,
553                    &event.params,
554                ) {
555                    continue;
556                }
557
558                let can_run = self.can_run_trigger(
559                    game,
560                    card_id,
561                    trigger_index,
562                    host_controller,
563                    &event.mode,
564                    &event.params,
565                );
566                if can_run {
567                    let sa = trigger.build_triggered_spell_ability(
568                        game,
569                        card_id,
570                        host_controller,
571                        trigger_index,
572                        &event.params,
573                    );
574
575                    let entry = StackEntry {
576                        id: 0,
577                        spell_ability: sa,
578                        is_pending_cast: false,
579                        is_creature_spell: false,
580                        is_permanent_spell: false,
581                        cast_from_zone: None,
582                        optional_trigger_decider: None,
583                        optional_trigger_description: None,
584                        optional_trigger_source_name: None,
585                    };
586                    // A trigger is optional if it has OptionalDecider$ OR if its
587                    // execute SVar has a non-mandatory, non-zero cost.  Mirrors Java's
588                    // Trigger.isOptional() which checks both the trigger flag and cost.
589                    // E.g. Smuggler's Copter loot "Cost$ Draw<1/You>" → optional.
590                    let trigger_cost_optional = entry
591                        .spell_ability
592                        .pay_costs
593                        .as_ref()
594                        .map(|c| !c.mandatory && !c.is_zero_cost())
595                        .unwrap_or(false);
596                    let effect_optional_decider =
597                        entry.spell_ability.ir.optional_decider_text.is_some();
598                    let description_lower = trigger.description.to_ascii_lowercase();
599                    let description_implies_optional = description_lower.starts_with("you may")
600                        && !entry.spell_ability.ir.optional
601                        && !effect_optional_decider;
602                    let pending = PendingTrigger {
603                        ability_triggered: Some(
604                            crate::trigger::trigger_ability_triggered::build_run_params(
605                                trigger,
606                                &entry.spell_ability,
607                                &event.params,
608                                game,
609                            ),
610                        ),
611                        entry,
612                        optional: (trigger.optional && !effect_optional_decider)
613                            || trigger_cost_optional
614                            || description_implies_optional,
615                        decider: host_controller,
616                        description: trigger.description.clone(),
617                    };
618                    let source_ts = card.zone_timestamp;
619                    entries.push((pending, host_controller, source_ts, 1, trigger.id));
620                    let extra = crate::staticability::static_ability_panharmonicon::extra_triggers(
621                        game,
622                        card_id,
623                        trigger,
624                        &event.params,
625                    );
626                    for _ in 0..extra {
627                        let sa2 = trigger.build_triggered_spell_ability(
628                            game,
629                            card_id,
630                            host_controller,
631                            trigger_index,
632                            &event.params,
633                        );
634                        let extra_entry = StackEntry {
635                            id: 0,
636                            spell_ability: sa2,
637                            is_pending_cast: false,
638                            is_creature_spell: false,
639                            is_permanent_spell: false,
640                            cast_from_zone: None,
641                            optional_trigger_decider: None,
642                            optional_trigger_description: None,
643                            optional_trigger_source_name: None,
644                        };
645                        let trigger_cost_optional = extra_entry
646                            .spell_ability
647                            .pay_costs
648                            .as_ref()
649                            .map(|c| !c.mandatory && !c.is_zero_cost())
650                            .unwrap_or(false);
651                        let effect_optional_decider =
652                            extra_entry.spell_ability.ir.optional_decider_text.is_some();
653                        entries.push((
654                            PendingTrigger {
655                                ability_triggered: Some(
656                                    crate::trigger::trigger_ability_triggered::build_run_params(
657                                        trigger,
658                                        &extra_entry.spell_ability,
659                                        &event.params,
660                                        game,
661                                    ),
662                                ),
663                                entry: extra_entry,
664                                optional: (trigger.optional && !effect_optional_decider)
665                                    || trigger_cost_optional,
666                                decider: host_controller,
667                                description: trigger.description.clone(),
668                            },
669                            host_controller,
670                            source_ts,
671                            1,
672                            trigger.id,
673                        ));
674                    }
675                }
676            }
677
678            // Check delayed triggers (one-shot, removed after firing).
679            //
680            // Java places same-event delayed triggers such as Evoke's sacrifice
681            // below normal ETB triggers by default, so the normal ETB resolves
682            // first. Since the stack is LIFO and pending triggers are pushed in
683            // list order, insert delayed matches before active matches for this
684            // event. Delayed triggers marked `sort_after_active` (granted-evoke
685            // sac) instead push AFTER active triggers and resolve first, mirroring
686            // Java's trigger-ID ascending sort when the granted keyword's trigger
687            // ID is higher than the card's intrinsic T: triggers.
688            let delayed_insert_at = event_entries_start;
689            let mut delayed_insert_offset = 0usize;
690            let mut fired_indices = Vec::new();
691            for (idx, delayed) in self.delayed_triggers.iter().enumerate() {
692                let TriggerWaiting {
693                    params: event_payload,
694                    ..
695                } = &event;
696                if delayed.mode != event.mode {
697                    continue;
698                }
699                if delayed.mode == TriggerType::Phase
700                    && delayed.created_turn == game.turn.turn_number
701                    && event_payload.phase == Some(delayed.created_phase)
702                {
703                    continue;
704                }
705                let tmp_trigger = delayed.as_trigger(game);
706                if !delayed
707                    .trigger_mode
708                    .perform_test(&tmp_trigger, event_payload, game)
709                {
710                    continue;
711                }
712                let mut sa = build_spell_ability(
713                    game,
714                    delayed.source_card,
715                    &delayed.execute_svar,
716                    delayed.controller,
717                );
718                sa.is_trigger = true;
719                sa.trigger_source = Some(delayed.source_card);
720                sa.trigger_source_zone_timestamp =
721                    Some(game.card(delayed.source_card).zone_timestamp);
722                sa.trigger_remembered_amount = delayed.remembered_amount;
723                // Propagate remembered cards (e.g. `RememberObjects$ Remembered`
724                // captured at registration) so the executed ability can target
725                // exactly the cards the parent trigger remembered.
726                sa.trigger_remembered.extend(
727                    delayed
728                        .remembered_cards
729                        .iter()
730                        .copied()
731                        .map(crate::event::AbilityValue::Card),
732                );
733                // Players from `RememberObjects$ RememberedController` (e.g.
734                // Arcane Denial's "may draw up to two cards" trigger snapshots
735                // the controller of the countered spell at registration).
736                sa.trigger_remembered.extend(
737                    delayed
738                        .remembered_players
739                        .iter()
740                        .copied()
741                        .map(crate::event::AbilityValue::Player),
742                );
743                if !delayed.remembered_lki_cards.is_empty() {
744                    sa.trigger_remembered.extend(
745                        delayed
746                            .remembered_lki_cards
747                            .iter()
748                            .copied()
749                            .map(crate::event::AbilityValue::Card),
750                    );
751                    sa.trigger_objects.insert(
752                        crate::ability::AbilityKey::RememberedLKI,
753                        delayed
754                            .remembered_lki_cards
755                            .iter()
756                            .map(|card_id| card_id.0.to_string())
757                            .collect::<Vec<_>>()
758                            .join(",")
759                            .into(),
760                    );
761                }
762
763                let entry = StackEntry {
764                    id: 0,
765                    spell_ability: sa,
766                    is_pending_cast: false,
767                    is_creature_spell: false,
768                    is_permanent_spell: false,
769                    cast_from_zone: None,
770                    optional_trigger_decider: None,
771                    optional_trigger_description: None,
772                    optional_trigger_source_name: None,
773                };
774                let pending = PendingTrigger {
775                    ability_triggered: Some(
776                        crate::trigger::trigger_ability_triggered::build_run_params(
777                            &tmp_trigger,
778                            &entry.spell_ability,
779                            event_payload,
780                            game,
781                        ),
782                    ),
783                    entry,
784                    optional: false,
785                    decider: delayed.controller,
786                    description: String::new(),
787                };
788                let delayed_ts = if delayed.mode == TriggerType::Phase {
789                    0
790                } else {
791                    game.card(delayed.source_card).zone_timestamp
792                };
793                let delayed_bucket = if delayed.sort_after_active { 2 } else { 0 };
794                let delayed_order = delayed.trigger_order.unwrap_or(0);
795                // Java parity: Panharmonicon-class statics (e.g. Yarok, Roaming Throne)
796                // double triggered abilities of permanents the owner controls. Rust
797                // implements Evoke-style sacrifice and other keyword-induced triggers
798                // as DelayedTrigger instances; the non-delayed path at ~L580 already
799                // consults `extra_triggers`, but delayed matches bypassed it until now.
800                // Replicate the same check so that when Yarok's own Panharmonicon
801                // fires during its Evoke-on-ETB sacrifice, it doubles the trigger
802                // (matching Java's `amt=2` on `valid=Card.Self+evoked`).
803                let extra_delayed =
804                    crate::staticability::static_ability_panharmonicon::extra_triggers(
805                        game,
806                        delayed.source_card,
807                        &tmp_trigger,
808                        event_payload,
809                    );
810                if delayed.sort_after_active {
811                    // Push to end of entries (above active triggers → resolves first).
812                    entries.push((
813                        pending.clone(),
814                        delayed.controller,
815                        delayed_ts,
816                        delayed_bucket,
817                        delayed_order,
818                    ));
819                    for _ in 0..extra_delayed {
820                        entries.push((
821                            pending.clone(),
822                            delayed.controller,
823                            delayed_ts,
824                            delayed_bucket,
825                            delayed_order,
826                        ));
827                    }
828                } else {
829                    entries.insert(
830                        delayed_insert_at + delayed_insert_offset,
831                        (
832                            pending.clone(),
833                            delayed.controller,
834                            delayed_ts,
835                            delayed_bucket,
836                            delayed_order,
837                        ),
838                    );
839                    delayed_insert_offset += 1;
840                    for _ in 0..extra_delayed {
841                        entries.insert(
842                            delayed_insert_at + delayed_insert_offset,
843                            (
844                                pending.clone(),
845                                delayed.controller,
846                                delayed_ts,
847                                delayed_bucket,
848                                delayed_order,
849                            ),
850                        );
851                        delayed_insert_offset += 1;
852                    }
853                }
854                fired_indices.push(idx);
855            }
856
857            // Remove fired delayed triggers (reverse order to preserve indices).
858            for idx in fired_indices.into_iter().rev() {
859                self.delayed_triggers.remove(idx);
860            }
861        }
862
863        entries
864    }
865
866    fn build_waiting_trigger(&self, mode: TriggerType, params: RunParams) -> TriggerWaiting {
867        // Snapshot active triggers for events whose matching must reflect the
868        // state AT event time, not at flush time. Java evaluates SpellCast
869        // triggers immediately against triggers active on the battlefield —
870        // a spell's own `TriggerZones$ Battlefield` trigger cannot fire for
871        // its own cast because the card is still on the stack when the event
872        // dispatches. Rust flushes later, so without a snapshot the newly
873        // battlefield-registered self-trigger would incorrectly match.
874        let snapshot_active = matches!(
875            mode,
876            TriggerType::ChangesZone
877                | TriggerType::ChangesZoneAll
878                | TriggerType::Drawn
879                | TriggerType::SpellCast
880                | TriggerType::SpellAbilityCast
881                | TriggerType::SpellCastAll
882                | TriggerType::SpellCastOnce
883                | TriggerType::SpellCastOfType
884                | TriggerType::SpellCopied
885                | TriggerType::SpellCopy
886                | TriggerType::SpellAbilityCopy
887                | TriggerType::SpellCastOrCopy
888                | TriggerType::AbilityCast
889        );
890        let trigger_refs = if snapshot_active {
891            let snap: Vec<(CardId, usize)> = self
892                .active_triggers
893                .iter()
894                .map(|active| (active.card_id, active.trigger_index))
895                .collect();
896            Some(snap)
897        } else {
898            None
899        };
900
901        TriggerWaiting {
902            mode,
903            params,
904            trigger_refs,
905        }
906    }
907
908    /// Fire Immediate delayed triggers — these fire on the next trigger
909    /// processing cycle without needing a matching event. Mirrors Java's
910    /// `TriggerImmediate` which has `performTest()` returning true always.
911    fn fire_immediate_delayed_triggers(&mut self, game: &GameState) -> Vec<MatchedTrigger> {
912        let mut entries = Vec::new();
913        let mut fired_indices = Vec::new();
914        for (idx, delayed) in self.delayed_triggers.iter().enumerate() {
915            if delayed.mode != TriggerType::Immediate {
916                continue;
917            }
918            // Look up the Execute SVar on the source card
919            let svar_text = game
920                .card(delayed.source_card)
921                .svars
922                .get(&delayed.execute_svar)
923                .cloned();
924            if let Some(text) = svar_text {
925                let mut sa =
926                    build_spell_ability(game, delayed.source_card, &text, delayed.controller);
927                sa.is_trigger = true;
928                sa.trigger_source = Some(delayed.source_card);
929                sa.trigger_source_zone_timestamp =
930                    Some(game.card(delayed.source_card).zone_timestamp);
931                if !delayed.remembered_lki_cards.is_empty() {
932                    sa.trigger_remembered.extend(
933                        delayed
934                            .remembered_lki_cards
935                            .iter()
936                            .copied()
937                            .map(crate::event::AbilityValue::Card),
938                    );
939                    sa.trigger_objects.insert(
940                        crate::ability::AbilityKey::RememberedLKI,
941                        delayed
942                            .remembered_lki_cards
943                            .iter()
944                            .map(|card_id| card_id.0.to_string())
945                            .collect::<Vec<_>>()
946                            .join(",")
947                            .into(),
948                    );
949                }
950
951                let entry = StackEntry {
952                    id: 0,
953                    spell_ability: sa,
954                    is_pending_cast: false,
955                    is_creature_spell: false,
956                    is_permanent_spell: false,
957                    cast_from_zone: None,
958                    optional_trigger_decider: None,
959                    optional_trigger_description: None,
960                    optional_trigger_source_name: None,
961                };
962                let pending = PendingTrigger {
963                    ability_triggered: Some(
964                        crate::trigger::trigger_ability_triggered::build_run_params(
965                            &delayed.as_trigger(game),
966                            &entry.spell_ability,
967                            &RunParams::default(),
968                            game,
969                        ),
970                    ),
971                    entry,
972                    optional: false,
973                    decider: delayed.controller,
974                    description: String::new(),
975                };
976                let ts = if delayed.mode == TriggerType::Phase {
977                    0
978                } else {
979                    game.card(delayed.source_card).zone_timestamp
980                };
981                let trigger_bucket = if delayed.sort_after_active { 2 } else { 0 };
982                entries.push((
983                    pending,
984                    delayed.controller,
985                    ts,
986                    trigger_bucket,
987                    delayed.trigger_order.unwrap_or(0),
988                ));
989                fired_indices.push(idx);
990            }
991        }
992        for idx in fired_indices.into_iter().rev() {
993            self.delayed_triggers.remove(idx);
994        }
995        entries
996    }
997
998    /// Register a delayed trigger (one-shot, fires once then is removed).
999    /// Mirrors Java's `TriggerHandler.registerDelayedTrigger()`.
1000    pub fn register_delayed_trigger(&mut self, mut delayed: DelayedTrigger) {
1001        self.assign_delayed_trigger_order(&mut delayed);
1002        self.delayed_triggers.push(delayed);
1003    }
1004
1005    pub fn clear_delayed_trigger(&mut self) {
1006        self.delayed_triggers.clear();
1007        self.this_turn_delayed_triggers.clear();
1008    }
1009
1010    pub fn register_this_turn_delayed_trigger(&mut self, mut delayed: DelayedTrigger) {
1011        self.assign_delayed_trigger_order(&mut delayed);
1012        self.this_turn_delayed_triggers.push(delayed.clone());
1013        self.delayed_triggers.push(delayed);
1014    }
1015
1016    pub fn clear_this_turn_delayed_trigger(&mut self) {
1017        if self.this_turn_delayed_triggers.is_empty() {
1018            return;
1019        }
1020        self.delayed_triggers.retain(|d| {
1021            !self.this_turn_delayed_triggers.iter().any(|x| {
1022                x.source_card == d.source_card
1023                    && x.controller == d.controller
1024                    && x.mode == d.mode
1025                    && x.execute_svar == d.execute_svar
1026            })
1027        });
1028        self.this_turn_delayed_triggers.clear();
1029    }
1030
1031    pub fn register_player_defined_delayed_trigger(
1032        &mut self,
1033        player: PlayerId,
1034        mut delayed: DelayedTrigger,
1035    ) {
1036        self.assign_delayed_trigger_order(&mut delayed);
1037        self.player_defined_delayed_triggers.push((player, delayed));
1038    }
1039
1040    pub fn clear_player_defined_delayed_trigger(&mut self) {
1041        self.player_defined_delayed_triggers.clear();
1042    }
1043
1044    pub fn handle_player_defined_del_triggers(&mut self, player: PlayerId) {
1045        let mut to_activate = Vec::new();
1046        self.player_defined_delayed_triggers.retain(|(p, delayed)| {
1047            if *p == player {
1048                to_activate.push(delayed.clone());
1049                false
1050            } else {
1051                true
1052            }
1053        });
1054        for delayed in to_activate {
1055            self.delayed_triggers.push(delayed);
1056        }
1057    }
1058
1059    fn assign_delayed_trigger_order(&mut self, delayed: &mut DelayedTrigger) {
1060        if delayed.trigger_order.is_none() {
1061            delayed.trigger_order = Some(self.next_trigger_id);
1062            self.next_trigger_id += 1;
1063        }
1064    }
1065
1066    /// Mirrors Java's resetActiveTriggers().
1067    /// Scans all cards in game, collects active triggers.
1068    pub fn reset_active_triggers(&mut self, game: &GameState) {
1069        self.active_triggers.clear();
1070        for (i, card) in game.cards.iter().enumerate() {
1071            let card_id = CardId(i as u32);
1072            for (trig_idx, _) in card.triggers.iter().enumerate() {
1073                self.register_one_trigger(game, card_id, trig_idx);
1074            }
1075        }
1076    }
1077
1078    /// Mirrors Java's registerActiveTrigger(card).
1079    /// Registers a single card's triggers.
1080    pub fn register_active_trigger(&mut self, game: &GameState, card_id: CardId) {
1081        let card = game.card(card_id);
1082        for (trig_idx, _) in card.triggers.iter().enumerate() {
1083            self.register_one_trigger(game, card_id, trig_idx);
1084        }
1085    }
1086
1087    /// Java parity wrapper for TriggerHandler.clearActiveTriggers().
1088    pub fn clear_active_triggers(&mut self) {
1089        self.active_triggers.clear();
1090    }
1091
1092    /// Java parity wrapper for TriggerHandler.registerActiveLTBTrigger(card).
1093    /// Registers all of a card's triggers as active, using current trigger indices.
1094    pub fn register_active_ltb_trigger(&mut self, game: &GameState, card_id: CardId) {
1095        let card = game.card(card_id);
1096        for (trig_idx, trigger) in card.triggers.iter().enumerate() {
1097            if self.looks_back_in_time(trigger) {
1098                self.register_one_trigger(game, card_id, trig_idx);
1099            }
1100        }
1101    }
1102
1103    /// Java parity wrapper for TriggerHandler.registerOneTrigger(...).
1104    pub fn register_one_trigger(
1105        &mut self,
1106        game: &GameState,
1107        card_id: CardId,
1108        trigger_index: usize,
1109    ) {
1110        let card = game.card(card_id);
1111        if trigger_index >= card.triggers.len() {
1112            return;
1113        }
1114        let trigger = &card.triggers[trigger_index];
1115        if !trigger.get_active_zone().contains(&card.zone) {
1116            return;
1117        }
1118        // NOTE: Do NOT call phases_check here.  Phase-gated triggers (e.g.
1119        // "At the beginning of your upkeep") must be registered as active
1120        // regardless of the current phase; the phase filter is evaluated at
1121        // match time inside can_run_trigger.
1122        if trigger.kind == TriggerType::Always && game.stack.has_state_trigger_id(trigger.id) {
1123            return;
1124        }
1125        let already_registered = self
1126            .active_triggers
1127            .iter()
1128            .any(|at| at.card_id == card_id && at.trigger_index == trigger_index);
1129        if !already_registered {
1130            if std::env::var("FORGE_TRIGGER_TRACE").is_ok() {
1131                if let Some(trigger) = game.card(card_id).triggers.get(trigger_index) {
1132                    if trigger.kind == TriggerType::BecomesTarget {
1133                        eprintln!(
1134                            "[trigger-trace] REGISTER active BecomesTarget trigger host={}#{:?} idx={} desc={} zones={:?}",
1135                            game.card(card_id).card_name,
1136                            card_id,
1137                            trigger_index,
1138                            trigger.description,
1139                            trigger.get_active_zone()
1140                        );
1141                    }
1142                }
1143            }
1144            self.active_triggers.push(ActiveTrigger {
1145                card_id,
1146                trigger_index,
1147            });
1148        }
1149    }
1150
1151    /// Remove triggers for a card that left a trigger zone.
1152    pub fn unregister_active_triggers(&mut self, card_id: CardId) {
1153        self.active_triggers.retain(|at| at.card_id != card_id);
1154    }
1155
1156    fn looks_back_in_time(&self, trigger: &Trigger) -> bool {
1157        if matches!(
1158            trigger.kind,
1159            TriggerType::Exploited
1160                | TriggerType::Destroyed
1161                | TriggerType::Sacrificed
1162                | TriggerType::SacrificedOnce
1163        ) {
1164            return true;
1165        }
1166        if matches!(
1167            trigger.kind,
1168            TriggerType::ChangesZone | TriggerType::ChangesZoneAll
1169        ) {
1170            return trigger
1171                .ir
1172                .origin_zones
1173                .contains(&forge_foundation::ZoneType::Battlefield)
1174                || trigger.ir.destination_zones.iter().any(|zone| {
1175                    matches!(
1176                        zone,
1177                        forge_foundation::ZoneType::Library | forge_foundation::ZoneType::Hand
1178                    )
1179                });
1180        }
1181        false
1182    }
1183
1184    fn ltb_trigger_refs_for_event(
1185        &self,
1186        game: &GameState,
1187        event: &TriggerWaiting,
1188    ) -> Vec<(CardId, usize)> {
1189        let TriggerWaiting {
1190            params: event_payload,
1191            ..
1192        } = event;
1193        match event.mode {
1194            TriggerType::ChangesZone => {
1195                let destination = event_payload.destination;
1196                if event_payload.origin != Some(forge_foundation::ZoneType::Battlefield)
1197                    && !matches!(
1198                        destination,
1199                        Some(
1200                            forge_foundation::ZoneType::Library | forge_foundation::ZoneType::Hand
1201                        )
1202                    )
1203                {
1204                    return Vec::new();
1205                }
1206                let Some(card_id) = event_payload.card_lki.or(event_payload.card) else {
1207                    return Vec::new();
1208                };
1209                let mut refs: Vec<(CardId, usize)> = game
1210                    .card(card_id)
1211                    .triggers
1212                    .iter()
1213                    .enumerate()
1214                    .filter_map(|(trigger_index, trigger)| {
1215                        self.looks_back_in_time(trigger)
1216                            .then_some((card_id, trigger_index))
1217                    })
1218                    .collect();
1219
1220                // Same-SBA-batch LTB lookback: cards that were on the battlefield
1221                // when this SBA batch started but are no longer there can still
1222                // observe deaths happening in the same batch (e.g. Blood Artist
1223                // seeing Savannah Lions die simultaneously). Mirrors Java's
1224                // triggerChangesZoneAll which re-registers LTB triggers from
1225                // lastStateBattlefield. Scoping via pre_sba_battlefield prevents
1226                // stale hosts from earlier batches firing for later deaths.
1227                if event_payload.origin == Some(forge_foundation::ZoneType::Battlefield) {
1228                    for &ltb_id in &game.pre_sba_battlefield {
1229                        if ltb_id == card_id {
1230                            continue;
1231                        }
1232                        let ltb_card = game.card(ltb_id);
1233                        if ltb_card.zone == forge_foundation::ZoneType::Battlefield {
1234                            // Still alive; active_triggers already covers it.
1235                            continue;
1236                        }
1237                        for (trigger_index, trigger) in ltb_card.triggers.iter().enumerate() {
1238                            if self.looks_back_in_time(trigger) {
1239                                refs.push((ltb_id, trigger_index));
1240                            }
1241                        }
1242                    }
1243                }
1244
1245                refs
1246            }
1247            TriggerType::ChangesZoneAll => event
1248                .params
1249                .change_zone_table
1250                .as_ref()
1251                .map(|table| {
1252                    table
1253                        .last_state_battlefield()
1254                        .iter()
1255                        .flat_map(|&card_id| {
1256                            game.card(card_id).triggers.iter().enumerate().filter_map(
1257                                move |(trigger_index, trigger)| {
1258                                    self.looks_back_in_time(trigger)
1259                                        .then_some((card_id, trigger_index))
1260                                },
1261                            )
1262                        })
1263                        .collect()
1264                })
1265                .unwrap_or_default(),
1266            _ => Vec::new(),
1267        }
1268    }
1269
1270    pub fn clear_waiting_triggers(&mut self) {
1271        self.waiting_triggers.clear();
1272        self.pre_matched_triggers.clear();
1273    }
1274
1275    pub fn on_player_lost(&mut self, player: PlayerId) {
1276        self.delayed_triggers.retain(|d| d.controller != player);
1277        self.this_turn_delayed_triggers
1278            .retain(|d| d.controller != player);
1279        self.player_defined_delayed_triggers
1280            .retain(|(p, _)| *p != player);
1281    }
1282
1283    /// Mirrors Java's StaticAbilityDisableTriggers.disabled().
1284    /// Checks if a ChangesZone trigger is suppressed by a DisableTriggers
1285    /// static ability (e.g. Hushbringer).
1286    fn is_trigger_disabled_by_static(
1287        game: &GameState,
1288        host_card: CardId,
1289        trigger_index: usize,
1290        params: &RunParams,
1291    ) -> bool {
1292        let trigger = &game.card(host_card).triggers[trigger_index];
1293
1294        // Only applies to ChangesZone triggers
1295        if trigger.kind != TriggerType::ChangesZone {
1296            return false;
1297        }
1298
1299        // The card changing zones (the "cause")
1300        let cause_card_id = match params.card {
1301            Some(cid) => cid,
1302            None => return false,
1303        };
1304
1305        // Check cards for DisableTriggers static abilities.
1306        // For LTB triggers (Origin=Battlefield), also check cards that were on the
1307        // battlefield at the start of the current SBA check (pre_sba_battlefield).
1308        // This handles the case where a DisableTriggers source (e.g. Hushbringer)
1309        // dies in the same SBA batch as the trigger source — Hushbringer was on the
1310        // battlefield when the batch started, so its DisableTriggers still applies.
1311        // Mirrors Java's StaticAbilityDisableTriggers.disabled() which uses
1312        // lastStateBattlefield for LTB triggers (lines 20-27).
1313        let is_ltb = params.origin == Some(ZoneType::Battlefield);
1314        for card in game.cards.iter() {
1315            let on_battlefield = card.zone == ZoneType::Battlefield;
1316            let was_on_battlefield = is_ltb && game.pre_sba_battlefield.contains(&card.id);
1317            if !on_battlefield && !was_on_battlefield {
1318                continue;
1319            }
1320            for sa in &card.static_abilities {
1321                if !sa.check_mode(&crate::staticability::StaticMode::DisableTriggers) {
1322                    continue;
1323                }
1324
1325                // ValidCause$ — must match the card changing zones
1326                if let Some(valid_cause) = sa.ir.valid_cause.as_ref() {
1327                    let context = valid_filter::MatchContext::from_source(card)
1328                        .with_game(game)
1329                        .with_triggering(cause_card_id.into(), params.player);
1330                    if !valid_filter::matches_valid_card_selector_with_context(
1331                        valid_cause,
1332                        game.card(cause_card_id),
1333                        context,
1334                    ) {
1335                        continue;
1336                    }
1337                }
1338
1339                // ValidMode$ — must match the trigger's mode
1340                if let Some(valid_mode) = sa.ir.valid_mode.as_deref() {
1341                    let modes: Vec<&str> = valid_mode.split(',').collect();
1342                    if !modes.contains(&"ChangesZone") {
1343                        continue;
1344                    }
1345                }
1346
1347                // Origin$ — the event's origin zone must match
1348                if !sa.ir.origin_zones.is_empty()
1349                    && !params
1350                        .origin
1351                        .is_some_and(|zone| sa.ir.origin_zones.contains(&zone))
1352                {
1353                    continue;
1354                }
1355
1356                // Destination$ — the event's destination zone must match
1357                if !sa.ir.destination_zones.is_empty()
1358                    && !params
1359                        .destination
1360                        .is_some_and(|zone| sa.ir.destination_zones.contains(&zone))
1361                {
1362                    continue;
1363                }
1364
1365                // All conditions matched — trigger is disabled.
1366                return true;
1367            }
1368        }
1369        false
1370    }
1371
1372    /// Mirrors Java's canRunTrigger().
1373    /// Validation chain: mode → suppression → active zones → performTest.
1374    fn can_run_trigger(
1375        &self,
1376        game: &GameState,
1377        host_card: CardId,
1378        trigger_index: usize,
1379        host_controller: PlayerId,
1380        mode: &TriggerType,
1381        params: &RunParams,
1382    ) -> bool {
1383        let _perf_scope =
1384            crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Trigger);
1385        let card = game.card(host_card);
1386        if trigger_index >= card.triggers.len() {
1387            return false;
1388        }
1389        let trigger = &card.triggers[trigger_index];
1390
1391        // Check mode matches trigger type
1392        let trigger_type = trigger.mode.trigger_type();
1393
1394        let normalize = |t: TriggerType| -> TriggerType {
1395            match t {
1396                TriggerType::DungeonCompleted => TriggerType::CompletedDungeon,
1397                TriggerType::RoomEntered => TriggerType::EnteredRoom,
1398                TriggerType::Airbend
1399                | TriggerType::Earthbend
1400                | TriggerType::Firebend
1401                | TriggerType::Waterbend
1402                | TriggerType::ElementalBend => TriggerType::Elementalbend,
1403                TriggerType::TakesInitiative => TriggerType::TakeInitiative,
1404                TriggerType::PhaseIn => TriggerType::PhasedIn,
1405                TriggerType::PhaseOut => TriggerType::PhasedOut,
1406                TriggerType::PlaneswalkedFrom | TriggerType::PlaneswalkedTo => {
1407                    TriggerType::Planeswalk
1408                }
1409                TriggerType::CrankContraption => TriggerType::CrankAdvanced,
1410                TriggerType::Explores => TriggerType::Explored,
1411                TriggerType::Unattach => TriggerType::Unattached,
1412                TriggerType::Saddled | TriggerType::Stationed => TriggerType::Crewed,
1413                other => other,
1414            }
1415        };
1416        let mode_matches = normalize(trigger_type) == normalize(*mode);
1417        if !mode_matches {
1418            return false;
1419        }
1420
1421        // Check suppression
1422        if self.is_trigger_suppressed(*mode) {
1423            return false;
1424        }
1425
1426        // Common trigger phase/requirement/limit checks (Java Trigger base behavior).
1427        if !trigger.phases_check(game, host_card, params.phase) {
1428            return false;
1429        }
1430        if !trigger.requirements_check(game, host_card) {
1431            return false;
1432        }
1433        if !trigger.check_activation_limit(game, host_card) {
1434            return false;
1435        }
1436
1437        // DisableTriggers static ability check (e.g. Hushbringer).
1438        // Mirrors Java's StaticAbilityDisableTriggers.disabled().
1439        if matches!(
1440            *mode,
1441            TriggerType::ChangesZone | TriggerType::ChangesZoneAll
1442        ) && Self::is_trigger_disabled_by_static(game, host_card, trigger_index, params)
1443        {
1444            return false;
1445        }
1446
1447        // Check active zones.
1448        // For self zone-change events, use origin as LKI zone so "dies" triggers
1449        // (active on Battlefield) still fire after the card has moved.
1450        let zone_for_active_check = if *mode == TriggerType::ChangesZone
1451            && params.card == Some(host_card)
1452            && params.origin == Some(ZoneType::Battlefield)
1453            && params.destination != Some(ZoneType::Battlefield)
1454        {
1455            // LKI active-zone check for "leaves battlefield" self triggers (e.g. dies).
1456            ZoneType::Battlefield
1457        } else if *mode == TriggerType::ChangesZone
1458            && params.card == Some(host_card)
1459            && params.destination == Some(ZoneType::Battlefield)
1460            && trigger.get_active_zone().contains(&ZoneType::Battlefield)
1461            && card.zone != ZoneType::Battlefield
1462        {
1463            // LKI active-zone check for ETB triggers whose host left before
1464            // waiting triggers were put on the stack.
1465            ZoneType::Battlefield
1466        } else if *mode == TriggerType::ChangesZone
1467            && params.origin == Some(ZoneType::Battlefield)
1468            && trigger.get_active_zone().contains(&ZoneType::Battlefield)
1469            && card.zone != ZoneType::Battlefield
1470            && self.looks_back_in_time(trigger)
1471            && game.pre_sba_battlefield.contains(&host_card)
1472        {
1473            // LKI active-zone check for LTB triggers seeing OTHER creatures die
1474            // in the same SBA batch (e.g. Blood Artist seeing Savannah Lions die).
1475            // Scoped via pre_sba_battlefield so that hosts that left earlier
1476            // batches do not fire for later deaths. Mirrors Java's
1477            // lastStateBattlefield boundary.
1478            ZoneType::Battlefield
1479        } else if (*mode == TriggerType::DamageDone || *mode == TriggerType::DamageDoneOnce)
1480            && params.damage_target_card == Some(host_card)
1481            && trigger.get_active_zone().contains(&ZoneType::Battlefield)
1482            && card.zone != ZoneType::Battlefield
1483        {
1484            // LKI for DamageDone/DamageDoneOnce triggers targeting self (e.g.
1485            // Raptor Hatchling enrage). When damage kills the creature, SBAs
1486            // move it to graveyard before triggers are processed. The trigger
1487            // was queued while the card was on the battlefield, so we use LKI.
1488            ZoneType::Battlefield
1489        } else if *mode == TriggerType::Exploited
1490            && params.card == Some(host_card)
1491            && trigger.get_active_zone().contains(&ZoneType::Battlefield)
1492            && card.zone != ZoneType::Battlefield
1493        {
1494            // LKI for Exploited triggers: the exploiting creature sacrificed
1495            // itself. It was on the battlefield when Exploit fired, so use LKI.
1496            ZoneType::Battlefield
1497        } else if *mode == TriggerType::Sacrificed
1498            && params.card == Some(host_card)
1499            && trigger.get_active_zone().contains(&ZoneType::Battlefield)
1500            && card.zone != ZoneType::Battlefield
1501        {
1502            // LKI for Sacrificed triggers: the creature was on the battlefield
1503            // when it was sacrificed, use LKI.
1504            ZoneType::Battlefield
1505        } else {
1506            card.zone
1507        };
1508        if !trigger.get_active_zone().contains(&zone_for_active_check) {
1509            return false;
1510        }
1511
1512        // Phased-out hosts do not see events (CR 702.26.5). Mirrors Java
1513        // `TriggerReplacementBase.zonesCheck` which gates on `!isPhasedOut()`.
1514        if card.phased_out {
1515            return false;
1516        }
1517
1518        // performTest
1519        if !trigger.mode.perform_test(trigger, params, game) {
1520            return false;
1521        }
1522        if !trigger.meets_requirements_on_triggered_objects(game, params, host_card) {
1523            return false;
1524        }
1525
1526        // ── ActivatorThisTurnCast$ condition ──────────────────────────
1527        // Mirrors Java's TriggerSpellAbilityCast.checkActivatorThisTurnCast():
1528        // "EQ2" means the activating player must have cast exactly 2 spells
1529        // this turn (including the one triggering).
1530        if let Some(cond) = trigger.ir.activator_this_turn_cast.as_deref() {
1531            let caster = params.spell_controller.unwrap_or(host_controller);
1532            let count = game.player(caster).spells_cast_this_turn;
1533            if !compare_expr(count, cond.trim()) {
1534                return false;
1535            }
1536        }
1537
1538        true
1539    }
1540
1541    pub fn suppress_mode(&mut self, mode: TriggerType) {
1542        self.suppressed_modes.insert(mode);
1543    }
1544
1545    pub fn set_suppress_all_triggers(&mut self, suppress: bool) {
1546        self.all_suppressed = suppress;
1547    }
1548
1549    /// Mirrors Java's TriggerHandler.getActiveTrigger(TriggerType, Map<AbilityKey, Object>).
1550    /// Returns all active triggers that can run for the given mode and params.
1551    pub fn get_active_trigger(
1552        &self,
1553        game: &GameState,
1554        mode: TriggerType,
1555        params: &RunParams,
1556    ) -> Vec<Trigger> {
1557        let mut result = Vec::new();
1558        for at in &self.active_triggers {
1559            let card = game.card(at.card_id);
1560            if at.trigger_index >= card.triggers.len() {
1561                continue;
1562            }
1563            let host_controller = card.controller;
1564            if self.can_run_trigger(
1565                game,
1566                at.card_id,
1567                at.trigger_index,
1568                host_controller,
1569                &mode,
1570                params,
1571            ) {
1572                result.push(card.triggers[at.trigger_index].clone());
1573            }
1574        }
1575        result
1576    }
1577
1578    pub fn is_trigger_suppressed(&self, mode: TriggerType) -> bool {
1579        self.all_suppressed || self.suppressed_modes.contains(&mode)
1580    }
1581
1582    pub fn clear_suppression(&mut self, mode: TriggerType) {
1583        self.suppressed_modes.remove(&mode);
1584    }
1585}
1586
1587impl Default for TriggerHandler {
1588    fn default() -> Self {
1589        Self::new()
1590    }
1591}
1592
1593#[cfg(test)]
1594mod tests {
1595    use super::*;
1596    use crate::card::Card;
1597    use crate::event::RunParams;
1598    use crate::ids::PlayerId;
1599    use crate::trigger::TriggerType;
1600    use forge_carddb::parse_card_script;
1601    use forge_foundation::PhaseType;
1602
1603    #[test]
1604    fn maralen_style_draw_phase_trigger_is_active() {
1605        let rules = parse_card_script(
1606            "Name:Maralen of the Mornsong\nManaCost:1 B B\nTypes:Legendary Creature Elf Wizard\nPT:2/3\nT:Mode$ Phase | Phase$ Draw | ValidPlayer$ Player | TriggerZones$ Battlefield | Execute$ TrigDrain | TriggerDescription$ At the beginning of each player's draw step, that player loses 3 life, searches their library for a card, puts it into their hand, then shuffles.\nSVar:TrigDrain:DB$ LoseLife | Defined$ TriggeredPlayer | LifeAmount$ 3",
1607        )
1608        .expect("card script should parse");
1609
1610        let mut game = GameState::new(&["Alice", "Bob"], 20);
1611        let mut maralen = Card::from_rules(&rules, PlayerId(0));
1612        maralen.id = crate::ids::CardId(0);
1613        maralen.zone = forge_foundation::ZoneType::Battlefield;
1614        game.cards.push(maralen);
1615        game.add_card_to_zone(
1616            forge_foundation::ZoneType::Battlefield,
1617            PlayerId(0),
1618            crate::ids::CardId(0),
1619        );
1620
1621        let mut handler = TriggerHandler::new();
1622        handler.register_active_trigger(&game, crate::ids::CardId(0));
1623
1624        let active = handler.get_active_trigger(
1625            &game,
1626            TriggerType::Phase,
1627            &RunParams {
1628                phase: Some(PhaseType::Draw),
1629                player: Some(PlayerId(0)),
1630                ..Default::default()
1631            },
1632        );
1633
1634        assert_eq!(game.card(crate::ids::CardId(0)).triggers.len(), 1);
1635        assert_eq!(active.len(), 1);
1636
1637        let pending = handler.run_trigger_with_game(
1638            &game,
1639            TriggerType::Phase,
1640            RunParams {
1641                phase: Some(PhaseType::Draw),
1642                player: Some(PlayerId(0)),
1643                ..Default::default()
1644            },
1645            false,
1646        );
1647        assert_eq!(pending.len(), 1);
1648        assert_eq!(
1649            pending[0]
1650                .entry
1651                .spell_ability
1652                .get_triggering_player(crate::ability::AbilityKey::TriggeredPlayer),
1653            Some(PlayerId(0))
1654        );
1655    }
1656}