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 #[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 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 pub fn host_card_id(&self) -> CardId {
104 self.base.card_trait_base.host_card_id()
105 }
106
107 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
173impl 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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, card: &Card) -> bool {
959 self.get_chapter()
960 .is_some_and(|chapter| chapter == card.get_final_chapter_nr())
961 }
962
963 pub fn while_keyword_check(&self, _param: &str, _run_params: &RunParams) -> bool {
964 false
966 }
967
968 pub fn set_triggering_objects(
969 &self,
970 sa: &mut SpellAbility,
971 params: &RunParams,
972 game: &GameState,
973 _host_card: CardId,
974 _host_controller: PlayerId,
975 ) {
976 params.add_common_trigger_objects(sa);
977 self.mode.set_triggering_objects(self, sa, params, game);
978 }
979
980 pub fn build_triggered_spell_ability(
981 &self,
982 game: &GameState,
983 host_card: CardId,
984 host_controller: PlayerId,
985 trigger_index: usize,
986 params: &RunParams,
987 ) -> SpellAbility {
988 let host = game.card(host_card);
989 let (mut sa, svar_text) = if let Some(overriding_ability) = self.get_overriding_ability() {
990 (overriding_ability.clone(), String::new())
991 } else {
992 let svar_text = host.get_s_var(&self.execute).map(str::to_string).unwrap_or_else(|| {
993 panic!(
994 "Trigger::build_triggered_spell_ability missing/empty Execute SVar: host={} execute={} trigger_index={} description={}",
995 host.card_name,
996 self.execute,
997 trigger_index,
998 self.description
999 )
1000 });
1001 (
1002 build_spell_ability(game, host_card, &svar_text, host_controller),
1003 svar_text,
1004 )
1005 };
1006 sa.is_trigger = true;
1007 sa.trigger_source = Some(host_card);
1008 sa.trigger_source_zone_timestamp = Some(game.card(host_card).zone_timestamp);
1009 sa.source_trigger_id = Some(self.id);
1010 sa.trigger_index = Some(trigger_index);
1011 sa.trigger_remembered = self.trigger_remembered.clone();
1012 self.set_triggering_objects(&mut sa, params, game, host_card, host_controller);
1013 self.configure_triggered_spell_ability(&mut sa, params, game, &svar_text);
1014 sa
1015 }
1016
1017 fn configure_triggered_spell_ability(
1018 &self,
1019 sa: &mut SpellAbility,
1020 params: &RunParams,
1021 game: &GameState,
1022 svar_text: &str,
1023 ) {
1024 if let Some(pid) = params.damage_target_player {
1025 sa.target_chosen.target_player = Some(pid);
1026 }
1027 if sa.target_chosen.target_player.is_none() && svar_text.contains("TriggeredPlayer") {
1028 if let Some(pid) = params.player {
1029 sa.target_chosen.target_player = Some(pid);
1030 }
1031 }
1032 if let Some(pid) = params.defending_player {
1033 if sa.target_chosen.target_player.is_none() && svar_text.contains("DefendingPlayer") {
1034 sa.target_chosen.target_player = Some(pid);
1035 }
1036 }
1037 if let Some(cid) = params.damage_target_card {
1038 sa.target_chosen.target_card = Some(cid);
1039 }
1040 if let Some(cause_cid) = params.cause_card {
1041 if let Some(entry) = game.stack.find_by_source_card(cause_cid) {
1042 sa.target_chosen.target_stack_entry = Some(entry.id);
1043 }
1044 }
1045 if let Some(attacker_id) = params.attacker {
1046 if svar_text.contains("TriggeredAttacker") {
1047 sa.target_chosen.target_card = Some(attacker_id);
1048 }
1049 }
1050 if let Some(blocker_id) = params.blocker {
1051 if svar_text.contains("TriggeredBlocker") {
1052 sa.target_chosen.target_card = Some(blocker_id);
1053 }
1054 }
1055 if let Some(lki_p1p1) = params.lki_p1p1_counters {
1056 if self.execute.contains("Modular") || svar_text.contains("Modular") {
1057 sa.trigger_remembered_amount = lki_p1p1;
1058 }
1059 }
1060 }
1061}
1062
1063pub fn parse_trigger(raw: &str, next_id: &mut u32) -> Option<Trigger> {
1066 let _perf_scope =
1067 crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Trigger);
1068 let params = Params::from_raw(raw);
1069
1070 let mode_str = params.get(keys::MODE)?;
1071 let mode: Box<dyn TriggerBehavior> = match mode_str {
1072 "ChangesZone" => crate::trigger::trigger_changes_zone::TriggerChangesZone::parse(¶ms),
1073 "Phase" => crate::trigger::trigger_phase::TriggerPhase::parse(¶ms),
1074 "SpellCast" | "AbilityCast" | "SpellAbilityCast" => {
1075 crate::trigger::trigger_spell_ability_cast_or_copy::TriggerSpellAbilityCastOrCopy::parse(mode_str, ¶ms)
1076 }
1077 "Attacks" => crate::trigger::trigger_attacks::TriggerAttacks::parse(¶ms),
1078 "Fight" => crate::trigger::trigger_fight::TriggerFight::parse(¶ms),
1079 "FightOnce" => crate::trigger::trigger_fight_once::TriggerFightOnce::parse(¶ms),
1080 "DamageDone" => crate::trigger::trigger_damage_done::TriggerDamageDone::parse(¶ms),
1081 "Countered" => {
1082 let valid_card = params.selector_cloned(keys::VALID_CARD);
1083 let valid_cause = params.selector_cloned(keys::VALID_CAUSE);
1084 let valid_sa = params.get_cloned(keys::VALID_SA);
1085 crate::trigger::trigger_countered::TriggerCountered::parse(valid_card, valid_cause, valid_sa)
1086 }
1087 "Blocks" => crate::trigger::trigger_blocks::TriggerBlocks::parse(¶ms),
1088 "AttackerBlocked" => crate::trigger::trigger_attacker_blocked::TriggerAttackerBlocked::parse(¶ms),
1089 "AttackerUnblocked" => crate::trigger::trigger_attacker_unblocked::TriggerAttackerUnblocked::parse(¶ms),
1090 "LifeGained" => crate::trigger::trigger_life_gained::TriggerLifeGained::parse(¶ms),
1091 "LifeLost" => crate::trigger::trigger_life_lost::TriggerLifeLost::parse(¶ms),
1092 "CounterAdded" => crate::trigger::trigger_counter_added::TriggerCounterAdded::parse(¶ms),
1093 "CounterRemoved" => crate::trigger::trigger_counter_removed::TriggerCounterRemoved::parse(¶ms),
1094 "Sacrificed" => crate::trigger::trigger_sacrificed::TriggerSacrificed::parse(¶ms),
1095 "Drawn" => crate::trigger::trigger_drawn::TriggerDrawn::parse(¶ms),
1096 "Milled" => crate::trigger::trigger_milled::TriggerMilled::parse(¶ms),
1097 "Taps" => crate::trigger::trigger_taps::TriggerTaps::parse(¶ms),
1098 "Untaps" => crate::trigger::trigger_untaps::TriggerUntaps::parse(¶ms),
1099 "Transformed" => crate::trigger::trigger_transformed::TriggerTransformed::parse(¶ms),
1100 "TurnFaceUp" => crate::trigger::trigger_turn_face_up::TriggerTurnFaceUp::parse(¶ms),
1101 "Attached" => crate::trigger::trigger_attached::TriggerAttached::parse(¶ms),
1102 "Unattached" => crate::trigger::trigger_unattach::TriggerUnattach::parse(¶ms),
1103 "LandPlayed" => crate::trigger::trigger_land_played::TriggerLandPlayed::parse(¶ms),
1104 "BecomesTarget" => crate::trigger::trigger_becomes_target::TriggerBecomesTarget::parse(¶ms),
1105 "TapsForMana" => crate::trigger::trigger_taps_for_mana::TriggerTapsForMana::parse(¶ms),
1106 "AbilityActivated" => crate::trigger::trigger_ability_activated::TriggerAbilityActivated::parse(¶ms),
1107 "Explored" | "Explores" => crate::trigger::trigger_explores::TriggerExplores::parse(¶ms),
1108 "BecomeMonstrous" => crate::trigger::trigger_become_monstrous::TriggerBecomeMonstrous::parse(¶ms),
1109 "BecomeMonarch" => crate::trigger::trigger_become_monarch::TriggerBecomeMonarch::parse(¶ms),
1110 "DamageDealtOnce" => crate::trigger::trigger_damage_dealt_once::TriggerDamageDealtOnce::parse(¶ms),
1111 "Destroyed" => crate::trigger::trigger_destroyed::TriggerDestroyed::parse(¶ms),
1112 "Exiled" => crate::trigger::trigger_exiled::TriggerExiled::parse(¶ms),
1113 "CollectEvidence" => {
1114 let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1115 crate::trigger::trigger_collect_evidence::TriggerCollectEvidence::parse(valid_player)
1116 }
1117 "Forage" => crate::trigger::trigger_forage::TriggerForage::parse(¶ms),
1118 "Enlisted" => crate::trigger::trigger_enlisted::TriggerEnlisted::parse(¶ms),
1119 "FlippedCoin" => crate::trigger::trigger_flipped_coin::TriggerFlippedCoin::parse(¶ms),
1120 "RolledDie" => crate::trigger::trigger_rolled_die::TriggerRolledDie::parse(¶ms),
1121 "RolledDieOnce" => crate::trigger::trigger_rolled_die_once::TriggerRolledDieOnce::parse(¶ms),
1122 "TokenCreated" => crate::trigger::trigger_token_created::TriggerTokenCreated::parse(¶ms),
1123 "SpellCastOrCopy" | "SpellCopied" | "SpellAbilityCopy" | "SpellCopy" => {
1124 crate::trigger::trigger_spell_ability_cast_or_copy::TriggerSpellAbilityCastOrCopy::parse(mode_str, ¶ms)
1125 }
1126 "AttackersDeclared" | "AttackersDeclaredOneTarget" => {
1127 crate::trigger::trigger_attackers_declared::TriggerAttackersDeclared::parse(mode_str, ¶ms)
1128 }
1129 "BlockersDeclared" => crate::trigger::trigger_blockers_declared::TriggerBlockersDeclared::parse(¶ms),
1130 "ChangesZoneAll" => crate::trigger::trigger_changes_zone_all::TriggerChangesZoneAll::parse(¶ms),
1131 "ChangesController" => {
1132 let valid_card = params.selector_cloned(keys::VALID_CARD);
1133 crate::trigger::trigger_changes_controller::TriggerChangesController::parse(valid_card)
1134 }
1135 "TurnBegin" | "NewTurn" => crate::trigger::trigger_turn_begin::TriggerTurnBegin::parse(¶ms),
1136 "DamageDoneOnce" => crate::trigger::trigger_damage_done_once::TriggerDamageDoneOnce::parse(¶ms),
1137 "DamageDoneOnceByController" => crate::trigger::trigger_damage_done_once_by_controller::TriggerDamageDoneOnceByController::parse(¶ms),
1138 "SpellCastAll" => {
1139 crate::trigger::trigger_spell_ability_cast_or_copy::TriggerSpellAbilityCastOrCopy::parse(mode_str, ¶ms)
1140 }
1141 "LifeLostAll" => crate::trigger::trigger_life_lost_all::TriggerLifeLostAll::parse(¶ms),
1142 "CounterAddedOnce" => {
1143 let valid_card = params.selector_cloned(keys::VALID_CARD);
1144 let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1145 let valid_entity = params.selector_cloned("ValidEntity");
1146 let counter_type = params.get_cloned(keys::COUNTER_TYPE);
1147 let valid_source = params.selector_cloned(keys::VALID_SOURCE);
1148 let first_time_only = params.has("FirstTime");
1149 crate::trigger::trigger_counter_added_once::TriggerCounterAddedOnce::parse(valid_card, valid_player, valid_entity, counter_type, valid_source, first_time_only)
1150 }
1151 "CounterAddedAll" => {
1152 let counter_type = params.get_cloned(keys::COUNTER_TYPE);
1153 let valid = params.selector_cloned_any(&[keys::VALID, keys::VALID_CARD]);
1154 let valid_source = params.selector_cloned(keys::VALID_SOURCE);
1155 crate::trigger::trigger_counter_added_all::TriggerCounterAddedAll::parse(counter_type, valid, valid_source)
1156 }
1157 "CounterPlayerAddedAll" => {
1158 let valid_source = params.selector_cloned("ValidSource");
1159 let valid_object = params.selector_cloned("ValidObject");
1160 let valid_object_to_source = params.selector_cloned("ValidObjectToSource");
1161 crate::trigger::trigger_counter_player_added_all::TriggerCounterPlayerAddedAll::parse(valid_source, valid_object, valid_object_to_source)
1162 }
1163 "CounterTypeAddedAll" => {
1164 let valid_object = params.selector_cloned("ValidObject");
1165 let first_time_only = params.has("FirstTime");
1166 crate::trigger::trigger_counter_type_added_all::TriggerCounterTypeAddedAll::parse(valid_object, first_time_only)
1167 }
1168 "DiscardedAll" => crate::trigger::trigger_discarded_all::TriggerDiscardedAll::parse(¶ms),
1169 "Discarded" => crate::trigger::trigger_discarded::TriggerDiscarded::parse(¶ms),
1170 "SacrificedOnce" => crate::trigger::trigger_sacrificed_once::TriggerSacrificedOnce::parse(¶ms),
1171 "Cycled" | "Cycling" => crate::trigger::trigger_cycled::TriggerCycled::parse(¶ms),
1172 "PhasedIn" | "PhaseIn" => crate::trigger::trigger_phase_in::TriggerPhaseIn::parse(¶ms),
1173 "PhasedOut" | "PhaseOut" => crate::trigger::trigger_phase_out::TriggerPhaseOut::parse(¶ms),
1174 "Always" => crate::trigger::trigger_always::TriggerAlways::parse(¶ms),
1175 "Immediate" => crate::trigger::trigger_immediate::TriggerImmediate::parse(¶ms),
1176 "Surveil" => crate::trigger::trigger_surveil::TriggerSurveil::parse(¶ms),
1177 "Scry" => crate::trigger::trigger_scry::TriggerScry::parse(¶ms),
1178 "Foretell" | "Foretold" => crate::trigger::trigger_foretell::TriggerForetell::parse(¶ms),
1179 "SearchedLibrary" => crate::trigger::trigger_searched_library::TriggerSearchedLibrary::parse(¶ms),
1180 "Shuffled" => crate::trigger::trigger_shuffled::TriggerShuffled::parse(¶ms),
1181 "ManaAdded" => crate::trigger::trigger_mana_added::TriggerManaAdded::parse(¶ms),
1182 "TokenCreatedOnce" => crate::trigger::trigger_token_created_once::TriggerTokenCreatedOnce::parse(¶ms),
1183 "TapAll" => crate::trigger::trigger_tap_all::TriggerTapAll::parse(¶ms),
1184 "UntapAll" => crate::trigger::trigger_untap_all::TriggerUntapAll::parse(¶ms),
1185 "BecomesTargetOnce" => crate::trigger::trigger_becomes_target_once::TriggerBecomesTargetOnce::parse(¶ms),
1186 "AttackerBlockedByCreature" => crate::trigger::trigger_attacker_blocked_by_creature::TriggerAttackerBlockedByCreature::parse(¶ms),
1187 "AttackerBlockedOnce" => crate::trigger::trigger_attacker_blocked_once::TriggerAttackerBlockedOnce::parse(¶ms),
1188 "AttackerUnblockedOnce" => crate::trigger::trigger_attacker_unblocked_once::TriggerAttackerUnblockedOnce::parse(¶ms),
1189 "SpellCastOnce" | "SpellCastOfType" => {
1190 crate::trigger::trigger_spell_ability_cast_or_copy::TriggerSpellAbilityCastOrCopy::parse(mode_str, ¶ms)
1191 }
1192 "DamageAll" => crate::trigger::trigger_damage_all::TriggerDamageAll::parse(¶ms),
1193 "DamagePreventedOnce" => crate::trigger::trigger_damage_prevented_once::TriggerDamagePreventedOnce::parse(¶ms),
1194 "ExcessDamage" => crate::trigger::trigger_excess_damage::TriggerExcessDamage::parse(¶ms),
1195 "ExcessDamageAll" => crate::trigger::trigger_excess_damage_all::TriggerExcessDamageAll::parse(¶ms),
1196 "CounterRemovedOnce" => {
1197 let valid_card = params.selector_cloned(keys::VALID_CARD);
1198 let counter_type = params.get_cloned(keys::COUNTER_TYPE);
1199 crate::trigger::trigger_counter_removed_once::TriggerCounterRemovedOnce::parse(valid_card, counter_type)
1200 }
1201 "Exerted" => crate::trigger::trigger_exerted::TriggerExerted::parse(¶ms),
1202 "ManaExpend" => crate::trigger::trigger_mana_expend::TriggerManaExpend::parse(¶ms),
1203 "Mutates" => crate::trigger::trigger_mutates::TriggerMutates::parse(¶ms),
1204 "SetInMotion" => crate::trigger::trigger_set_in_motion::TriggerSetInMotion::parse(¶ms),
1205 "CaseSolved" => {
1206 let valid_card = params.selector_cloned(keys::VALID_CARD);
1207 let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1208 crate::trigger::trigger_case_solved::TriggerCaseSolved::parse(valid_card, valid_player)
1209 }
1210 "ClaimPrize" => {
1211 let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1212 let valid_card = params.selector_cloned(keys::VALID_CARD);
1213 crate::trigger::trigger_claim_prize::TriggerClaimPrize::parse(valid_player, valid_card)
1214 }
1215 "TakesInitiative" | "TakeInitiative" => crate::trigger::trigger_takes_initiative::TriggerTakesInitiative::parse(¶ms),
1216 "Adapt" => crate::trigger::trigger_adapt::TriggerAdapt::parse(¶ms),
1217 "BecomeRenowned" => crate::trigger::trigger_become_renowned::TriggerBecomeRenowned::parse(¶ms),
1218 "Evolved" => crate::trigger::trigger_evolved::TriggerEvolved::parse(¶ms),
1219 "BecomesPlotted" => crate::trigger::trigger_becomes_plotted::TriggerBecomesPlotted::parse(¶ms),
1220 "Investigated" => crate::trigger::trigger_investigated::TriggerInvestigated::parse(¶ms),
1221 "Proliferate" => crate::trigger::trigger_proliferate::TriggerProliferate::parse(¶ms),
1222 "CompletedDungeon" | "DungeonCompleted" => {
1223 let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1224 crate::trigger::trigger_completed_dungeon::TriggerCompletedDungeon::parse(valid_player)
1225 }
1226 "CommitCrime" => {
1227 let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1228 crate::trigger::trigger_commit_crime::TriggerCommitCrime::parse(valid_player)
1229 }
1230 "GiveGift" => crate::trigger::trigger_give_gift::TriggerGiveGift::parse(¶ms),
1231 "RingTemptsYou" => crate::trigger::trigger_ring_tempts_you::TriggerRingTemptsYou::parse(¶ms),
1232 "PayLife" => crate::trigger::trigger_pay_life::TriggerPayLife::parse(¶ms),
1233 "PayEcho" => crate::trigger::trigger_pay_echo::TriggerPayEcho::parse(¶ms),
1234 "ClassLevelGained" => {
1235 let valid_card = params.selector_cloned(keys::VALID_CARD);
1236 let class_level = params.as_i32("ClassLevel");
1237 crate::trigger::trigger_class_level_gained::TriggerClassLevelGained::parse(valid_card, class_level)
1238 }
1239 "NewGame" => crate::trigger::trigger_new_game::TriggerNewGame::parse(¶ms),
1240 "DayTimeChanges" => crate::trigger::trigger_day_time_changes::TriggerDayTimeChanges::parse(¶ms),
1241 "LosesGame" => crate::trigger::trigger_loses_game::TriggerLosesGame::parse(¶ms),
1242 "Discover" => crate::trigger::trigger_discover::TriggerDiscover::parse(¶ms),
1243 "Elementalbend" | "ElementalBend" | "Airbend" | "Earthbend" | "Firebend" | "Waterbend" => {
1244 crate::trigger::trigger_elementalbend::TriggerElementalbend::parse(¶ms)
1245 }
1246 "PlanarDice" => crate::trigger::trigger_planar_dice::TriggerPlanarDice::parse(¶ms),
1247 "PhaseOutAll" => crate::trigger::trigger_phase_out_all::TriggerPhaseOutAll::parse(¶ms),
1248 "VisitAttraction" => crate::trigger::trigger_visit_attraction::TriggerVisitAttraction::parse(¶ms),
1249 "EnteredRoom" | "RoomEntered" => crate::trigger::trigger_entered_room::TriggerEnteredRoom::parse(¶ms),
1250 "MilledAll" => crate::trigger::trigger_milled_all::TriggerMilledAll::parse(¶ms),
1251 "MilledOnce" => crate::trigger::trigger_milled_once::TriggerMilledOnce::parse(¶ms),
1252 "Abandoned" => crate::trigger::trigger_abandoned::TriggerAbandoned::parse(¶ms),
1253 "ManifestDread" => crate::trigger::trigger_manifest_dread::TriggerManifestDread::parse(¶ms),
1254 "Specializes" => crate::trigger::trigger_specializes::TriggerSpecializes::parse(¶ms),
1255 "Trains" => crate::trigger::trigger_trains::TriggerTrains::parse(¶ms),
1256 "Devoured" => crate::trigger::trigger_devoured::TriggerDevoured::parse(¶ms),
1257 "ConjureAll" => {
1258 let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1259 let valid_card = params.selector_cloned(keys::VALID_CARD);
1260 crate::trigger::trigger_conjure_all::TriggerConjureAll::parse(valid_player, valid_card)
1261 }
1262 "SeekAll" => crate::trigger::trigger_seek_all::TriggerSeekAll::parse(¶ms),
1263 "BecomesCrewed" => crate::trigger::trigger_becomes_crewed::TriggerBecomesCrewed::parse(¶ms),
1264 "Championed" => {
1265 let valid_card = params.selector_cloned_any(&["ValidCard", "ValidChampioned"]);
1266 let valid_source = params.selector_cloned("ValidSource");
1267 crate::trigger::trigger_championed::TriggerChampioned::parse(valid_card, valid_source)
1268 }
1269 "Clashed" => {
1270 let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1271 let won = params.get("Won").map(|v| v.eq_ignore_ascii_case("True"));
1272 crate::trigger::trigger_clashed::TriggerClashed::parse(valid_player, won)
1273 }
1274 "Mentored" => crate::trigger::trigger_mentored::TriggerMentored::parse(¶ms),
1275 "FullyUnlock" => crate::trigger::trigger_fully_unlock::TriggerFullyUnlock::parse(¶ms),
1276 "AbilityResolves" => crate::trigger::trigger_ability_resolves::TriggerAbilityResolves::parse(¶ms),
1277 "AbilityTriggered" => crate::trigger::trigger_ability_triggered::TriggerAbilityTriggered::parse(¶ms),
1278 "UnlockDoor" => crate::trigger::trigger_unlock_door::TriggerUnlockDoor::parse(¶ms),
1279 "Vote" => crate::trigger::trigger_vote::TriggerVote::parse(¶ms),
1280 "PlaneswalkedFrom" => crate::trigger::trigger_planeswalked_from::TriggerPlaneswalkedFrom::parse(¶ms),
1281 "PlaneswalkedTo" | "Planeswalk" => crate::trigger::trigger_planeswalked_to::TriggerPlaneswalkedTo::parse(¶ms),
1282 "CrankContraption" | "CrankAdvanced" => crate::trigger::trigger_crank_contraption::TriggerCrankContraption::parse(¶ms),
1283 "PayCumulativeUpkeep" => crate::trigger::trigger_pay_cumulative_upkeep::TriggerPayCumulativeUpkeep::parse(¶ms),
1284 "ChaosEnsues" => {
1285 let valid_player = params.selector_cloned(keys::VALID_PLAYER);
1286 crate::trigger::trigger_chaos_ensues::TriggerChaosEnsues::parse(valid_player)
1287 }
1288 "BecomesSaddled" => crate::trigger::trigger_becomes_saddled::TriggerBecomesSaddled::parse(¶ms),
1289 "Crewed" | "Saddled" | "Stationed" => crate::trigger::trigger_crewed_saddled::TriggerCrewedSaddled::parse(¶ms),
1290 "Unattach" => crate::trigger::trigger_unattach::TriggerUnattach::parse(¶ms),
1291 "Exploited" => crate::trigger::trigger_exploited::TriggerExploited::parse(¶ms),
1292 _ => return None,
1293 };
1294
1295 let optional = params.has(keys::OPTIONAL_DECIDER);
1296 let static_trigger = params.has("Static");
1297
1298 let mut active_zones = params
1300 .get(keys::TRIGGER_ZONES)
1301 .map(|s| {
1302 s.split(',')
1303 .filter_map(|z| ZoneType::from_str_compat(z.trim()))
1304 .collect::<Vec<_>>()
1305 })
1306 .unwrap_or_else(|| vec![ZoneType::Battlefield]);
1307 if matches!(
1312 mode.trigger_type(),
1313 TriggerType::SpellCast | TriggerType::SpellCastOrCopy
1314 ) {
1315 let valid_card_is_self = params
1316 .selector(keys::VALID_CARD)
1317 .map(|selector| selector.is_any_of(["Card.Self"]))
1318 .unwrap_or(false);
1319 if valid_card_is_self && !active_zones.contains(&ZoneType::Stack) {
1320 active_zones.push(ZoneType::Stack);
1321 }
1322 }
1323
1324 if !params.has(keys::TRIGGER_ZONES) && mode.trigger_type() == TriggerType::ChangesZone {
1325 if let Some(valid_card) = params.selector(keys::VALID_CARD) {
1326 let leaves_battlefield = params
1327 .get(keys::ORIGIN)
1328 .map(|origin| origin.split(',').any(|z| z.trim() == "Battlefield"))
1329 .unwrap_or(false);
1330 if leaves_battlefield && !static_trigger {
1331 active_zones = vec![ZoneType::Battlefield];
1332 }
1333
1334 let self_trigger = valid_card
1335 .alternatives
1336 .iter()
1337 .flat_map(|alternative| &alternative.parts)
1338 .any(|part| part.value.eq_ignore_ascii_case("Self"));
1339 let any_origin = params
1340 .get(keys::ORIGIN)
1341 .map(|origin| origin.eq_ignore_ascii_case("Any"))
1342 .unwrap_or(true);
1343 if self_trigger && any_origin {
1344 if let Some(destinations) = params.get(keys::DESTINATION) {
1345 let zones = destinations
1346 .split(',')
1347 .filter_map(|z| ZoneType::from_str_compat(z.trim()))
1348 .collect::<Vec<_>>();
1349 if !zones.is_empty() {
1350 active_zones = zones;
1351 }
1352 }
1353 }
1354 }
1355 }
1356
1357 let ir = TriggerIr::from_params(¶ms);
1358 let execute = params.get_cloned(keys::EXECUTE).unwrap_or_default();
1359 let description = params
1360 .get_cloned(keys::TRIGGER_DESCRIPTION)
1361 .unwrap_or_default();
1362
1363 let id = *next_id;
1364 *next_id += 1;
1365
1366 let mut base = TriggerReplacementBase::default();
1367 base.card_trait_base.set_id(id as i32);
1368 base.card_trait_base.set_map_params(
1369 params
1370 .inner()
1371 .iter()
1372 .map(|(key, value)| (key.clone(), value.clone()))
1373 .collect(),
1374 );
1375 base.card_trait_base.set_intrinsic(true);
1376 base.valid_host_zones = Some(active_zones);
1377
1378 let kind = mode.trigger_type();
1379 Some(Trigger {
1380 id,
1381 base,
1382 kind,
1383 mode,
1384 ir,
1385 execute,
1386 optional,
1387 description,
1388 static_trigger,
1389 trigger_remembered: Vec::new(),
1390 spawning_ability: None,
1391 })
1392}
1393
1394#[cfg(test)]
1395mod tests {
1396 use super::*;
1397 use crate::card::Card;
1398 use crate::event::RunParams;
1399 use crate::ids::{CardId, PlayerId};
1400 use crate::spellability::SpellAbility;
1401 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
1402
1403 #[test]
1404 fn parse_pipe_params_basic() {
1405 let params = Params::from_raw("Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDraw");
1406 assert_eq!(params.get("Mode"), Some("ChangesZone"));
1407 assert_eq!(params.get("Origin"), Some("Any"));
1408 assert_eq!(params.get("Destination"), Some("Battlefield"));
1409 assert_eq!(params.selector_value("ValidCard"), Some("Card.Self"));
1410 assert_eq!(params.get("Execute"), Some("TrigDraw"));
1411 }
1412
1413 #[test]
1414 fn parse_trigger_changes_zone() {
1415 let mut next_id = 0;
1416 let trigger = parse_trigger(
1417 "Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigDraw | TriggerDescription$ When CARDNAME enters the battlefield, draw two cards.",
1418 &mut next_id,
1419 ).unwrap();
1420
1421 assert_eq!(trigger.id, 0);
1422 assert_eq!(trigger.execute, "TrigDraw");
1423 assert_eq!(trigger.kind, TriggerType::ChangesZone);
1424 }
1425}