Skip to main content

manabrew_engine/staticability/
static_ability_disable_triggers.rs

1use forge_foundation::ZoneType;
2
3use crate::card::{valid_filter, Card};
4use crate::event::RunParams;
5use crate::game::GameState;
6use crate::ids::CardId;
7use crate::parsing::CompiledSelector;
8use crate::trigger::Trigger;
9use crate::trigger::TriggerType;
10
11pub fn is_disabled(
12    game: &GameState,
13    trigger_host: CardId,
14    regtrig: &Trigger,
15    _run_params: &RunParams,
16) -> bool {
17    let host = game.card(trigger_host);
18    for source in game
19        .cards
20        .iter()
21        .filter(|c| c.zone == ZoneType::Battlefield)
22    {
23        for st_ab in source
24            .static_abilities
25            .iter()
26            .filter(|sa| sa.check_mode(&crate::staticability::StaticMode::DisableTriggers))
27        {
28            if let Some(valid_mode) = st_ab.ir.valid_mode.as_deref() {
29                let modes = valid_mode.split(',').map(|s| s.trim());
30                let trig_mode = regtrig.kind.name();
31                if !modes.clone().any(|m| m.eq_ignore_ascii_case(trig_mode)) {
32                    continue;
33                }
34            }
35            if let Some(valid_card) = st_ab.ir.valid_card.as_ref() {
36                if !matches_valid_card(valid_card, host, source) {
37                    continue;
38                }
39            }
40            if let Some(valid_trigger) = st_ab.ir.valid_trigger.as_deref() {
41                if !trigger_matches(valid_trigger, game, trigger_host, regtrig) {
42                    continue;
43                }
44            }
45            if !mode_specific_matches(st_ab, game, regtrig, _run_params, source.controller) {
46                continue;
47            }
48            return true;
49        }
50    }
51    false
52}
53
54pub fn disabled(
55    game: &GameState,
56    trigger_host: CardId,
57    regtrig: &Trigger,
58    run_params: &RunParams,
59) -> bool {
60    is_disabled(game, trigger_host, regtrig, run_params)
61}
62
63fn mode_specific_matches(
64    st_ab: &crate::staticability::StaticAbility,
65    game: &GameState,
66    regtrig: &Trigger,
67    run_params: &RunParams,
68    source_controller: crate::ids::PlayerId,
69) -> bool {
70    match regtrig.kind {
71        TriggerType::ChangesZone => {
72            let origin = regtrig.origin_zone();
73            let moved = if origin == Some(ZoneType::Battlefield) {
74                run_params.card_lki
75            } else {
76                run_params.card
77            };
78            if let Some(valid_cause) = st_ab.ir.valid_cause.as_ref() {
79                let Some(cid) = moved else {
80                    return false;
81                };
82                if !matches_valid_card_for_controller(
83                    valid_cause,
84                    game.card(cid),
85                    source_controller,
86                ) {
87                    return false;
88                }
89            }
90            if !st_ab.ir.origin_zones.is_empty()
91                && !matches_zones(&st_ab.ir.origin_zones, run_params.origin)
92            {
93                return false;
94            }
95            if !st_ab.ir.destination_zones.is_empty()
96                && !matches_zones(&st_ab.ir.destination_zones, run_params.destination)
97            {
98                return false;
99            }
100            true
101        }
102        TriggerType::ChangesZoneAll => {
103            if let Some(valid_cause) = st_ab.ir.valid_cause.as_ref() {
104                let Some(cause_sa) = run_params.cause.as_ref() else {
105                    return false;
106                };
107                let Some(cause_card) = cause_sa.source else {
108                    return false;
109                };
110                if !matches_valid_card_for_controller(
111                    valid_cause,
112                    game.card(cause_card),
113                    source_controller,
114                ) {
115                    return false;
116                }
117            }
118            let Some(zone_changes) = run_params.zone_changes.as_ref() else {
119                return false;
120            };
121            let origin = regtrig.origin_zone();
122            let destination = regtrig.destination_zone();
123            zone_changes.iter().any(|zc| {
124                origin.is_none_or(|expected| zc.origin == expected)
125                    && destination.is_none_or(|expected| zc.destination == expected)
126            })
127        }
128        TriggerType::SpellCast
129        | TriggerType::AbilityCast
130        | TriggerType::SpellAbilityCast
131        | TriggerType::SpellCastOrCopy
132        | TriggerType::SpellCopied
133        | TriggerType::SpellCopy
134        | TriggerType::SpellAbilityCopy => {
135            if let Some(valid_cause) = st_ab.ir.valid_cause.as_ref() {
136                let Some(cid) = run_params.spell_card else {
137                    return false;
138                };
139                if !matches_valid_card_for_controller(
140                    valid_cause,
141                    game.card(cid),
142                    source_controller,
143                ) {
144                    return false;
145                }
146            }
147            if let Some(valid_activator) = st_ab.ir.valid_activator.as_ref() {
148                let Some(pid) = run_params.spell_controller else {
149                    return false;
150                };
151                if !matches_valid_player(valid_activator, pid, source_controller) {
152                    return false;
153                }
154            }
155            true
156        }
157        TriggerType::Attacks => {
158            if let Some(valid_cause) = st_ab.ir.valid_cause.as_ref() {
159                let Some(attacker) = run_params.attacker else {
160                    return false;
161                };
162                if !matches_valid_card_for_controller(
163                    valid_cause,
164                    game.card(attacker),
165                    source_controller,
166                ) {
167                    return false;
168                }
169            }
170            true
171        }
172        TriggerType::DamageDone | TriggerType::DamageDealtOnce => {
173            if let Some(wanted) = st_ab.ir.combat_damage {
174                if run_params.is_combat_damage != Some(wanted) {
175                    return false;
176                }
177            }
178            if let Some(valid_source) = st_ab.ir.valid_source.as_ref() {
179                let Some(source_id) = run_params.damage_source else {
180                    return false;
181                };
182                if !matches_valid_card_for_controller(
183                    valid_source,
184                    game.card(source_id),
185                    source_controller,
186                ) {
187                    return false;
188                }
189            }
190            if let Some(valid_target) = st_ab.ir.valid_target.as_ref() {
191                if let Some(target_card) = run_params.damage_target_card {
192                    if !matches_valid_card_for_controller(
193                        valid_target,
194                        game.card(target_card),
195                        source_controller,
196                    ) {
197                        return false;
198                    }
199                } else if let Some(target_player) = run_params.damage_target_player {
200                    if !matches_valid_player(valid_target, target_player, source_controller) {
201                        return false;
202                    }
203                }
204            }
205            true
206        }
207        _ => true,
208    }
209}
210
211fn matches_valid_card(valid: &CompiledSelector, card: &Card, source: &Card) -> bool {
212    valid_filter::matches_valid_card_selector(valid, card, source)
213}
214
215fn matches_valid_card_for_controller(
216    valid: &CompiledSelector,
217    card: &Card,
218    source_controller: crate::ids::PlayerId,
219) -> bool {
220    let mut dummy_source = card.clone();
221    dummy_source.controller = source_controller;
222    valid_filter::matches_valid_card_selector(valid, card, &dummy_source)
223}
224
225fn matches_valid_player(
226    valid: &CompiledSelector,
227    player: crate::ids::PlayerId,
228    source_controller: crate::ids::PlayerId,
229) -> bool {
230    valid_filter::matches_valid_player_selector(valid, player, source_controller)
231}
232
233fn trigger_matches(
234    valid_trigger: &str,
235    game: &GameState,
236    trigger_host: CardId,
237    regtrig: &Trigger,
238) -> bool {
239    let host = game.card(trigger_host);
240    let Some(exec_text) = host.svars.get(&regtrig.execute) else {
241        return false;
242    };
243    valid_trigger
244        .split(',')
245        .map(|s| s.trim())
246        .filter(|s| !s.is_empty())
247        .all(|tok| match tok.to_ascii_lowercase().as_str() {
248            "spell" => exec_text.trim_start().starts_with("SP$"),
249            "ability" => {
250                let trimmed = exec_text.trim_start();
251                trimmed.starts_with("AB$") || trimmed.starts_with("DB$")
252            }
253            "trigger" => true,
254            _ => true,
255        })
256}
257
258#[allow(dead_code)]
259fn matches_zone(filter: &str, zone: Option<ZoneType>) -> bool {
260    let Some(zone) = zone else {
261        return false;
262    };
263    match filter.to_ascii_lowercase().as_str() {
264        "battlefield" => zone == ZoneType::Battlefield,
265        "hand" => zone == ZoneType::Hand,
266        "graveyard" => zone == ZoneType::Graveyard,
267        "library" => zone == ZoneType::Library,
268        "exile" => zone == ZoneType::Exile,
269        "stack" => zone == ZoneType::Stack,
270        "command" => zone == ZoneType::Command,
271        "any" => true,
272        _ => true,
273    }
274}
275
276fn matches_zones(filters: &[ZoneType], zone: Option<ZoneType>) -> bool {
277    let Some(zone) = zone else {
278        return false;
279    };
280    filters.contains(&zone)
281}