manabrew_engine/staticability/
static_ability_disable_triggers.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 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(®trig.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}