manabrew_engine/trigger/
trigger_ability_triggered.rs1use serde::{Deserialize, Serialize};
2
3use super::trigger::TriggerBehavior;
4use crate::ability::AbilityKey;
5use crate::{
6 event::{AbilityValue, RunParams},
7 game::GameState,
8 ids::CardId,
9 parsing::Params,
10 spellability::SpellAbility,
11 trigger::TriggerType,
12};
13
14fn split_csv(s: &str) -> impl Iterator<Item = &str> {
15 s.split(',').map(str::trim).filter(|p| !p.is_empty())
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct TriggerAbilityTriggered {
20 pub valid_mode: Option<String>,
21 pub valid_destination: Option<String>,
22 pub valid_spell_ability: Option<String>,
23 pub valid_source: Option<crate::parsing::CompiledSelector>,
24 pub valid_cause: Option<crate::parsing::CompiledSelector>,
25 pub triggered_own_ability: bool,
26}
27
28impl TriggerAbilityTriggered {
29 pub fn parse(params: &Params) -> Box<dyn TriggerBehavior> {
30 Box::new(Self {
31 valid_mode: params.get_cloned("ValidMode"),
32 valid_destination: params.get_cloned("ValidDestination"),
33 valid_spell_ability: params.get_cloned("ValidSpellAbility"),
34 valid_source: params.selector_cloned("ValidSource"),
35 valid_cause: params.selector_cloned("ValidCause"),
36 triggered_own_ability: params.has("TriggeredOwnAbility"),
37 })
38 }
39}
40
41fn destination_names(params: &RunParams) -> Option<String> {
42 if let Some(destinations) = params.destinations.as_ref() {
43 return Some(destinations.clone());
44 }
45 if let Some(zone_changes) = params.zone_changes.as_ref() {
46 let mut ordered = Vec::new();
47 for change in zone_changes {
48 let name = format!("{:?}", change.destination);
49 if !ordered.contains(&name) {
50 ordered.push(name);
51 }
52 }
53 if !ordered.is_empty() {
54 return Some(ordered.join(","));
55 }
56 }
57 params
58 .destination
59 .map(|destination| format!("{destination:?}"))
60}
61
62fn cause_cards_for_trigger(
63 trigger: &super::trigger::Trigger,
64 params: &RunParams,
65 game: &GameState,
66) -> Vec<CardId> {
67 match trigger.kind {
68 TriggerType::ChangesZone => params.card.into_iter().collect(),
69 TriggerType::ChangesZoneAll => params
70 .change_zone_table
71 .as_ref()
72 .map(|table| table.all_cards())
73 .or_else(|| params.cards.clone())
74 .unwrap_or_default(),
75 TriggerType::Attacks => params.attacker.into_iter().collect(),
76 TriggerType::AttackersDeclared | TriggerType::AttackersDeclaredOneTarget => {
77 let attackers = params.attacker_ids.clone().unwrap_or_default();
78 if let Some(valid_attackers) = trigger.ir.valid_attackers_selector.as_ref() {
79 attackers
80 .into_iter()
81 .filter(|&card_id| {
82 trigger.matches_valid_card_filter(valid_attackers, card_id, game)
83 })
84 .collect()
85 } else {
86 attackers
87 }
88 }
89 _ => params
90 .cards
91 .clone()
92 .or_else(|| params.card.map(|card| vec![card]))
93 .unwrap_or_default(),
94 }
95}
96
97pub(crate) fn build_run_params(
98 trigger: &super::trigger::Trigger,
99 spell_ability: &SpellAbility,
100 params: &RunParams,
101 game: &GameState,
102) -> RunParams {
103 let destinations = destination_names(params);
104 let cause_cards = cause_cards_for_trigger(trigger, params, game);
105
106 RunParams {
107 spell_ability: Some(spell_ability.clone()),
108 source_sa: Some(spell_ability.clone()),
109 cause_card: spell_ability.source,
110 cards: if cause_cards.is_empty() {
111 None
112 } else {
113 Some(cause_cards)
114 },
115 mode: Some(trigger.kind.name().to_string()),
116 destination: destinations
117 .as_deref()
118 .and_then(|csv| csv.split(',').next())
119 .and_then(forge_foundation::ZoneType::from_str_compat),
120 destinations,
121 ..Default::default()
122 }
123}
124
125#[typetag::serde]
126impl TriggerBehavior for TriggerAbilityTriggered {
127 fn trigger_type(&self) -> TriggerType {
128 TriggerType::AbilityTriggered
129 }
130
131 fn perform_test(
132 &self,
133 trigger: &super::trigger::Trigger,
134 params: &RunParams,
135 game: &GameState,
136 ) -> bool {
137 let Some(AbilityValue::SpellAbility(sa)) = params.get_value(AbilityKey::SpellAbility)
138 else {
139 return false;
140 };
141 let Some(source) = sa.source else {
142 return false;
143 };
144
145 if let Some(valid_modes) = &self.valid_mode {
146 let Some(AbilityValue::String(mode_name)) = params.get_value(AbilityKey::Mode) else {
147 return false;
148 };
149 if !split_csv(valid_modes).any(|m| m.eq_ignore_ascii_case(&mode_name)) {
150 return false;
151 }
152 }
153
154 if let Some(valid_destinations) = &self.valid_destination {
155 let destinations = if let Some(AbilityValue::String(destinations)) =
156 params.get_value(AbilityKey::Destination)
157 {
158 destinations
159 } else if let Some(AbilityValue::Zone(dest)) = params.get_value(AbilityKey::Destination)
160 {
161 format!("{dest:?}")
162 } else {
163 return false;
164 };
165 if split_csv(&destinations).all(|destination_name| {
166 !split_csv(valid_destinations).any(|d| d.eq_ignore_ascii_case(destination_name))
167 }) {
168 return false;
169 }
170 }
171
172 if let Some(filter) = &self.valid_spell_ability {
173 if !trigger.matches_valid_sa_filter(filter, &sa) {
174 return false;
175 }
176 }
177
178 if !trigger.matches_optional_valid_card_filter(&self.valid_source, Some(source), game) {
179 return false;
180 }
181
182 let causes = match params.get_value(AbilityKey::Cause) {
183 Some(AbilityValue::Cards(cards)) => cards,
184 Some(AbilityValue::Card(card)) => vec![card],
185 _ => params.cards.clone().unwrap_or_default(),
186 };
187
188 if let Some(filter) = &self.valid_cause {
189 if causes.is_empty() {
190 return false;
191 }
192 if !causes.iter().any(|&c| {
193 trigger.matches_optional_valid_card_filter(&Some(filter.clone()), Some(c), game)
194 }) {
195 return false;
196 }
197 }
198
199 if self.triggered_own_ability && !causes.contains(&source) {
200 return false;
201 }
202
203 true
204 }
205
206 fn set_triggering_objects(
207 &self,
208 _trigger: &super::trigger::Trigger,
209 sa: &mut SpellAbility,
210 params: &RunParams,
211 _game: &GameState,
212 ) {
213 if let Some(ref triggered_sa) = params.spell_ability {
216 if let Some(source) = triggered_sa.source {
217 sa.set_triggering_object(crate::ability::AbilityKey::Source, source.0.to_string());
218 }
219 } else if let Some(card) = params.source_card {
220 sa.set_triggering_object(crate::ability::AbilityKey::Source, card.0.to_string());
221 }
222 if let Some(ref cause_cards) = params.cards {
225 let csv = cause_cards
226 .iter()
227 .map(|c| c.0.to_string())
228 .collect::<Vec<_>>()
229 .join(",");
230 sa.set_triggering_object(crate::ability::AbilityKey::Cause, &csv);
231 } else if let Some(cause_card) = params.cause_card {
232 sa.set_triggering_object(crate::ability::AbilityKey::Cause, cause_card.0.to_string());
233 }
234 }
235
236 fn get_important_stack_objects(
237 &self,
238 _trigger: &super::trigger::Trigger,
239 sa: &SpellAbility,
240 ) -> String {
241 format!(
242 "SpellAbility: {}",
243 sa.get_triggering_object(crate::ability::AbilityKey::SpellAbility)
244 .unwrap_or("")
245 )
246 }
247}