Skip to main content

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