Skip to main content

manabrew_engine/trigger/
trigger_ability_triggered.rs

1use 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        // Java: sa.setTriggeringObject(AbilityKey.Source, triggeredSA.getHostCard());
214        // The source is the host card of the triggered SpellAbility
215        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        // Java: sa.setTriggeringObjectsFrom(runParams, AbilityKey.SpellAbility, AbilityKey.Cause);
223        // SpellAbility and Cause are complex objects; store what we can
224        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}