manabrew_engine/staticability/
static_ability_panharmonicon.rs1use 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}