Skip to main content

manabrew_engine/ability/effects/
animate_all_effect.rs

1use forge_foundation::{ColorSet, ZoneType};
2
3use super::trait_animate_effect::parse_animate_params;
4use super::{matches_valid_cards_for_sa, EffectContext};
5use crate::card::card_trait_changes::CardTraitChanges;
6use crate::card::perpetual::perpetual_interface::PerpetualInterface;
7use crate::card::perpetual::{
8    perpetual_abilities, perpetual_colors, perpetual_incorporate, perpetual_keywords,
9    perpetual_mana_cost, perpetual_new_pt, perpetual_types,
10};
11use crate::card::AnimateState;
12use crate::parsing::keys;
13use crate::trigger::parse_trigger;
14use forge_foundation::ManaCost;
15
16/// `SP$ AnimateAll` — animate all matching permanents on the battlefield.
17///
18/// Mirrors Java's `AnimateAllEffect.java`.
19///
20/// # Params
21/// - `ValidCards` — filter for which cards to animate (e.g. "Land.YouCtrl", "Creature")
22/// - `Power` — set base power
23/// - `Toughness` — set base toughness
24/// - `Types` — comma-separated types to add (e.g. "Creature,Elemental")
25/// - `Keywords` — `&`-separated keywords to grant (until EOT)
26/// - `Colors` — comma-separated colors to set (e.g. "Blue")
27/// - `OverwriteColors` — if "True", replace color instead of adding
28/// - `RemoveCreatureTypes` — if "True", clear subtypes before adding new types
29/// - `RemoveAllAbilities` — if "True", clear all keywords/abilities
30/// Struct form of this effect so it can participate in the
31/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
32/// `AnimateAllEffect` class extending `SpellAbilityEffect`.
33#[manabrew_engine_macros::spell_effect(AnimateAllEffect)]
34fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
35    let valid_filter = sa
36        .ir
37        .valid_cards_text
38        .clone()
39        .unwrap_or_else(|| "Card".to_string());
40    let valid_selector = sa.ir.valid_cards_selector.clone();
41
42    // Use shared base-class parsing for common animate params
43    let anim_params = parse_animate_params(sa);
44    let power_str = anim_params.power.map(|p| p.to_string());
45    let toughness_str = anim_params.toughness.map(|t| t.to_string());
46    let types_str = if anim_params.add_types.is_empty() {
47        None
48    } else {
49        Some(anim_params.add_types.join(","))
50    };
51    // AnimateAll uses " & " as keyword separator (unlike Animate which uses ",")
52    // parse_animate_params splits on "," so we also split on "&" for AnimateAll-specific handling
53    let keywords_str = sa.ir.animate_keywords_text.clone();
54    let triggers_str = sa.ir.animate_triggers_text.clone();
55    let colors_str = anim_params.colors.map(|c| c.join(","));
56    let overwrite_colors = sa.param_is_true(keys::OVERWRITE_COLORS);
57    let remove_creature_types = sa.ir.animate_remove_creature_types;
58    let remove_all_abilities = sa.ir.animate_remove_all_abilities;
59    let incorporate_cost = sa.ir.animate_incorporate_text.clone();
60    let mana_cost_override = sa.ir.animate_mana_cost_override_text.clone();
61    let is_perpetual = matches!(
62        sa.ir.duration,
63        Some(crate::spellability::AbilityDuration::Perpetual)
64    );
65    let resolve_ts = ctx.game.next_effect_timestamp();
66
67    // Resolve Triggers$ SVars from source into parsed Trigger objects.
68    let mut parsed_triggers: Vec<crate::trigger::Trigger> = Vec::new();
69    if let Some(ref trigs) = triggers_str {
70        let source_id = sa.source.unwrap_or(crate::ids::CardId(0));
71        let source_svars = ctx.game.card(source_id).svars.clone();
72        let mut next_trig_id = 1000u32;
73        for trig_name in trigs.split(',') {
74            let trig_name = trig_name.trim();
75            if trig_name.is_empty() {
76                continue;
77            }
78            if let Some(svar_text) = source_svars.get(trig_name) {
79                if let Some(mut trig) = parse_trigger(svar_text, &mut next_trig_id) {
80                    trig.execute = trig.execute.clone();
81                    parsed_triggers.push(trig);
82                }
83            }
84        }
85    }
86
87    // Collect matching cards on the battlefield.
88    let player_ids = ctx.game.player_order.clone();
89    let mut targets = Vec::new();
90    for &pid in &player_ids {
91        let zone_cards = ctx.game.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
92        for cid in zone_cards {
93            if matches_valid_cards_for_sa(
94                ctx.game,
95                sa,
96                ctx.game.card(cid),
97                valid_selector.as_ref(),
98                &valid_filter,
99            ) {
100                targets.push(cid);
101            }
102        }
103    }
104
105    for card_id in targets {
106        if ctx.game.card(card_id).zone != ZoneType::Battlefield {
107            continue;
108        }
109
110        let effect_ts = if is_perpetual { Some(resolve_ts) } else { None };
111
112        if let Some(ref mc) = incorporate_cost {
113            let ts = resolve_ts;
114            perpetual_incorporate::PerpetualIncorporate {
115                timestamp: ts,
116                incorporate: ManaCost::parse(mc),
117            }
118            .apply_effect(ctx.game.card_mut(card_id));
119        }
120        if let Some(ref mc) = mana_cost_override {
121            if let Some(ts) = effect_ts {
122                perpetual_mana_cost::PerpetualManaCost {
123                    timestamp: ts,
124                    mana_cost: ManaCost::parse(mc),
125                }
126                .apply_effect(ctx.game.card_mut(card_id));
127            }
128        }
129
130        // Save original state (only if not already animated this turn)
131        if ctx.game.card(card_id).animate_state.is_none() {
132            let original_type_line = ctx.game.card(card_id).type_line.clone();
133            let original_base_power = ctx.game.card(card_id).base_power;
134            let original_base_toughness = ctx.game.card(card_id).base_toughness;
135            let original_color = ctx.game.card(card_id).color;
136            let original_keywords = ctx.game.card(card_id).keywords.clone();
137            ctx.game
138                .card_mut(card_id)
139                .set_animate_state(Some(AnimateState {
140                    original_type_line,
141                    original_base_power,
142                    original_base_toughness,
143                    original_color,
144                    original_keywords: Some(original_keywords),
145                }));
146        }
147
148        // RemoveAllAbilities — strip keywords/abilities
149        if remove_all_abilities {
150            ctx.game.card_mut(card_id).clear_all_keyword_sets();
151        }
152
153        // RemoveCreatureTypes — clear subtypes before adding new types
154        if remove_creature_types {
155            ctx.game.card_mut(card_id).clear_subtypes();
156        }
157
158        // Apply type changes
159        if let Some(ref types) = types_str {
160            for t in types.split(',') {
161                let t = t.trim();
162                if !t.is_empty() {
163                    if let Some(ts) = effect_ts {
164                        perpetual_types::PerpetualTypes {
165                            timestamp: ts,
166                            add_types: vec![t.to_string()],
167                        }
168                        .apply_effect(ctx.game.card_mut(card_id));
169                    } else {
170                        ctx.game.card_mut(card_id).add_type(t);
171                    }
172                }
173            }
174        }
175
176        // Apply P/T
177        let parsed_power = power_str
178            .as_deref()
179            .and_then(|p| p.trim().parse::<i32>().ok());
180        let parsed_toughness = toughness_str
181            .as_deref()
182            .and_then(|t| t.trim().parse::<i32>().ok());
183        if let Some(ts) = effect_ts {
184            if parsed_power.is_some() || parsed_toughness.is_some() {
185                perpetual_new_pt::PerpetualNewPt {
186                    timestamp: ts,
187                    power: parsed_power,
188                    toughness: parsed_toughness,
189                }
190                .apply_effect(ctx.game.card_mut(card_id));
191            }
192        } else {
193            if let Some(val) = parsed_power {
194                ctx.game.card_mut(card_id).set_base_power(Some(val));
195            }
196            if let Some(val) = parsed_toughness {
197                ctx.game.card_mut(card_id).set_base_toughness(Some(val));
198            }
199        }
200
201        // Apply keywords (until EOT — stored in pump_keywords so they get cleared at cleanup)
202        // AnimateAll uses " & " as keyword separator (unlike Animate which uses ",")
203        if let Some(ref kws) = keywords_str {
204            for kw in kws.split('&') {
205                let kw = kw.trim();
206                if !kw.is_empty() {
207                    if let Some(ts) = effect_ts {
208                        perpetual_keywords::PerpetualKeywords {
209                            timestamp: ts,
210                            add_keywords: vec![kw.to_string()],
211                            remove_keywords: Vec::new(),
212                            remove_all: false,
213                        }
214                        .apply_effect(ctx.game.card_mut(card_id));
215                    } else {
216                        ctx.game.card_mut(card_id).add_pump_keyword(kw);
217                    }
218                }
219            }
220        }
221
222        // Apply color
223        if let Some(ref colors) = colors_str {
224            let mut new_color = ColorSet::COLORLESS;
225            for c in colors.split(',') {
226                let c = c.trim().to_lowercase();
227                match c.as_str() {
228                    "white" | "w" => new_color = new_color.union(ColorSet::WHITE),
229                    "blue" | "u" => new_color = new_color.union(ColorSet::BLUE),
230                    "black" | "b" => new_color = new_color.union(ColorSet::BLACK),
231                    "red" | "r" => new_color = new_color.union(ColorSet::RED),
232                    "green" | "g" => new_color = new_color.union(ColorSet::GREEN),
233                    _ => {}
234                }
235            }
236            if overwrite_colors {
237                if let Some(ts) = effect_ts {
238                    perpetual_colors::PerpetualColors {
239                        timestamp: ts,
240                        colors: new_color,
241                        overwrite: true,
242                    }
243                    .apply_effect(ctx.game.card_mut(card_id));
244                } else {
245                    ctx.game.card_mut(card_id).set_color(new_color);
246                }
247            } else if let Some(ts) = effect_ts {
248                perpetual_colors::PerpetualColors {
249                    timestamp: ts,
250                    colors: new_color,
251                    overwrite: false,
252                }
253                .apply_effect(ctx.game.card_mut(card_id));
254            } else {
255                let union = ctx.game.card(card_id).color.union(new_color);
256                ctx.game.card_mut(card_id).set_color(union);
257            }
258        }
259
260        // Apply trigger trait changes from Triggers$
261        if !parsed_triggers.is_empty() {
262            let source_id = sa.source.unwrap_or(crate::ids::CardId(0));
263            let source_svars = ctx.game.card(source_id).svars.clone();
264            if let Some(ts) = effect_ts {
265                let changes = CardTraitChanges {
266                    triggers: parsed_triggers.clone(),
267                    ..Default::default()
268                };
269                perpetual_abilities::PerpetualAbilities {
270                    timestamp: ts,
271                    changes,
272                }
273                .apply_effect(ctx.game.card_mut(card_id));
274                for trig in &parsed_triggers {
275                    if !trig.execute.is_empty() {
276                        if let Some(exec_svar) = source_svars.get(&trig.execute) {
277                            ctx.game
278                                .card_mut(card_id)
279                                .set_s_var_if_absent(trig.execute.clone(), exec_svar.clone());
280                        }
281                    }
282                }
283            } else {
284                for trig in &parsed_triggers {
285                    ctx.game.card_mut(card_id).add_trigger(trig.clone());
286                    ctx.game.card_mut(card_id).increment_pump_trigger_count();
287                    if !trig.execute.is_empty() {
288                        if let Some(exec_svar) = source_svars.get(&trig.execute) {
289                            ctx.game
290                                .card_mut(card_id)
291                                .set_s_var_if_absent(trig.execute.clone(), exec_svar.clone());
292                        }
293                    }
294                }
295            }
296            ctx.trigger_handler
297                .register_active_trigger(ctx.game, card_id);
298        }
299    }
300}