manabrew_engine/ability/effects/
animate_effect.rs1use forge_foundation::{ColorSet, ZoneType};
2
3use super::trait_animate_effect::parse_animate_params;
4use super::EffectContext;
5use crate::ability::ability_ir::DefinedRef;
6use crate::card::card_trait_changes::CardTraitChanges;
7use crate::card::perpetual::perpetual_interface::PerpetualInterface;
8use crate::card::perpetual::{
9 perpetual_abilities, perpetual_colors, perpetual_incorporate, perpetual_keywords,
10 perpetual_mana_cost, perpetual_new_pt, perpetual_types,
11};
12use crate::card::AnimateState;
13use crate::spellability::SpellAbility;
14use crate::trigger::parse_trigger;
15use forge_foundation::ManaCost;
16
17#[manabrew_engine_macros::spell_effect(AnimateEffect)]
35fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
36 let controller = sa.activating_player;
37
38 let target_ids = resolve_animate_targets(ctx, sa, controller);
40
41 let anim_params = parse_animate_params(sa);
43 let power_str = anim_params.power.map(|p| p.to_string());
44 let toughness_str = anim_params.toughness.map(|t| t.to_string());
45 let types_str = if anim_params.add_types.is_empty() {
46 None
47 } else {
48 Some(anim_params.add_types.join(","))
49 };
50 let keywords_str = if anim_params.add_keywords.is_empty() {
51 None
52 } else {
53 Some(anim_params.add_keywords.join(","))
54 };
55 let remove_keywords_str = sa.ir.animate_remove_keywords_text.clone();
56 let colors_str = anim_params.colors.map(|c| c.join(","));
57 let triggers_str = sa.ir.animate_triggers_text.clone();
58 let overwrite_types = anim_params.overwrite_types;
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 is_permanent_duration = matches!(
66 sa.ir.duration,
67 Some(crate::spellability::AbilityDuration::Permanent)
68 );
69 let resolve_ts = ctx.game.next_effect_timestamp();
70
71 let mut parsed_triggers: Vec<crate::trigger::Trigger> = Vec::new();
74 if let Some(ref trigs) = triggers_str {
75 let source_id = sa.source.unwrap_or(crate::ids::CardId(0));
77 let source_svars = ctx.game.card(source_id).svars.clone();
78 let mut next_trig_id = 1000u32; for trig_name in trigs.split(',') {
80 let trig_name = trig_name.trim();
81 if trig_name.is_empty() {
82 continue;
83 }
84 if let Some(svar_text) = source_svars.get(trig_name) {
85 if let Some(mut trig) = parse_trigger(svar_text, &mut next_trig_id) {
86 trig.execute = trig.execute.clone();
87 parsed_triggers.push(trig);
88 }
89 }
90 }
91 }
92
93 let eot_targets = target_ids.clone();
95
96 for card_id in target_ids {
97 if ctx.game.card(card_id).zone != ZoneType::Battlefield {
99 continue;
100 }
101
102 let effect_ts = if is_perpetual { Some(resolve_ts) } else { None };
103
104 if let Some(ref mc) = incorporate_cost {
105 let ts = resolve_ts;
106 perpetual_incorporate::PerpetualIncorporate {
107 timestamp: ts,
108 incorporate: ManaCost::parse(mc),
109 }
110 .apply_effect(ctx.game.card_mut(card_id));
111 }
112 if let Some(ref mc) = mana_cost_override {
113 if let Some(ts) = effect_ts {
114 perpetual_mana_cost::PerpetualManaCost {
115 timestamp: ts,
116 mana_cost: ManaCost::parse(mc),
117 }
118 .apply_effect(ctx.game.card_mut(card_id));
119 }
120 }
121
122 if ctx.game.card(card_id).animate_state.is_none() {
124 let original_type_line = ctx.game.card(card_id).type_line.clone();
125 let original_base_power = ctx.game.card(card_id).base_power;
126 let original_base_toughness = ctx.game.card(card_id).base_toughness;
127 let original_color = ctx.game.card(card_id).color;
128 let original_keywords = ctx.game.card(card_id).keywords.clone();
129 ctx.game
130 .card_mut(card_id)
131 .set_animate_state(Some(AnimateState {
132 original_type_line,
133 original_base_power,
134 original_base_toughness,
135 original_color,
136 original_keywords: Some(original_keywords),
137 }));
138 }
139
140 if let Some(ref types) = types_str {
142 if overwrite_types {
143 ctx.game
144 .card_mut(card_id)
145 .set_type_line(forge_foundation::CardTypeLine::new());
146 }
147 for t in types.split(',') {
148 let t = t.trim();
149 if !t.is_empty() {
150 if let Some(ts) = effect_ts {
151 perpetual_types::PerpetualTypes {
152 timestamp: ts,
153 add_types: vec![t.to_string()],
154 }
155 .apply_effect(ctx.game.card_mut(card_id));
156 } else {
157 ctx.game.card_mut(card_id).add_type(t);
158 }
159 }
160 }
161 }
162
163 if sa
164 .ir
165 .spell_description_text
166 .as_deref()
167 .is_some_and(|desc| desc.to_ascii_lowercase().starts_with("crew"))
168 {
169 ctx.game.card_mut(card_id).becomes_crewed();
170 }
171
172 let parsed_power = power_str
174 .as_deref()
175 .and_then(|p| p.trim().parse::<i32>().ok());
176 let parsed_toughness = toughness_str
177 .as_deref()
178 .and_then(|t| t.trim().parse::<i32>().ok());
179 if let Some(ts) = effect_ts {
180 if parsed_power.is_some() || parsed_toughness.is_some() {
181 perpetual_new_pt::PerpetualNewPt {
182 timestamp: ts,
183 power: parsed_power,
184 toughness: parsed_toughness,
185 }
186 .apply_effect(ctx.game.card_mut(card_id));
187 }
188 } else {
189 if let Some(val) = parsed_power {
190 ctx.game.card_mut(card_id).set_base_power(Some(val));
191 }
192 if let Some(val) = parsed_toughness {
193 ctx.game.card_mut(card_id).set_base_toughness(Some(val));
194 }
195 }
196
197 let add_keywords: Vec<String> = keywords_str
201 .as_deref()
202 .map(|kws| {
203 kws.split(',')
204 .map(str::trim)
205 .filter(|kw| !kw.is_empty())
206 .map(str::to_string)
207 .collect()
208 })
209 .unwrap_or_default();
210 let remove_keywords: Vec<String> = remove_keywords_str
211 .as_deref()
212 .map(|kws| {
213 kws.split(',')
214 .map(str::trim)
215 .filter(|kw| !kw.is_empty())
216 .map(str::to_string)
217 .collect()
218 })
219 .unwrap_or_default();
220 if !add_keywords.is_empty() || !remove_keywords.is_empty() {
221 if let Some(ts) = effect_ts {
222 perpetual_keywords::PerpetualKeywords {
223 timestamp: ts,
224 add_keywords,
225 remove_keywords,
226 remove_all: false,
227 }
228 .apply_effect(ctx.game.card_mut(card_id));
229 } else if is_permanent_duration {
230 let card = ctx.game.card_mut(card_id);
231 for kw in &remove_keywords {
232 card.remove_changed_card_keywords(kw);
233 card.remove_hidden_extrinsic_keywords(kw);
234 card.pump_keywords.remove(kw);
235 }
236 for kw in &add_keywords {
237 card.add_changed_card_keywords(kw);
238 }
239 } else {
240 let card = ctx.game.card_mut(card_id);
241 for kw in &remove_keywords {
242 card.pump_keywords.remove(kw);
243 }
244 for kw in &add_keywords {
245 card.add_pump_keyword(kw);
246 }
247 }
248 }
249
250 if !parsed_triggers.is_empty() {
254 let source_id = sa.source.unwrap_or(crate::ids::CardId(0));
255 let source_svars = ctx.game.card(source_id).svars.clone();
256 if let Some(ts) = effect_ts {
257 let changes = CardTraitChanges {
258 triggers: parsed_triggers.clone(),
259 ..Default::default()
260 };
261 perpetual_abilities::PerpetualAbilities {
262 timestamp: ts,
263 changes,
264 }
265 .apply_effect(ctx.game.card_mut(card_id));
266 for trig in &parsed_triggers {
267 if !trig.execute.is_empty() {
268 if let Some(exec_svar) = source_svars.get(&trig.execute) {
269 ctx.game
270 .card_mut(card_id)
271 .set_s_var_if_absent(trig.execute.clone(), exec_svar.clone());
272 }
273 }
274 }
275 } else {
276 for trig in &parsed_triggers {
277 ctx.game.card_mut(card_id).add_trigger(trig.clone());
278 ctx.game.card_mut(card_id).increment_pump_trigger_count();
279 if !trig.execute.is_empty() {
282 if let Some(exec_svar) = source_svars.get(&trig.execute) {
283 ctx.game
284 .card_mut(card_id)
285 .set_s_var_if_absent(trig.execute.clone(), exec_svar.clone());
286 }
287 }
288 }
289 }
290 ctx.trigger_handler
292 .register_active_trigger(ctx.game, card_id);
293 }
294
295 if let Some(ref colors) = colors_str {
297 let mut new_color = ColorSet::COLORLESS;
298 for c in colors.split(',') {
299 let c = c.trim().to_lowercase();
300 match c.as_str() {
301 "white" | "w" => new_color = new_color.union(ColorSet::WHITE),
302 "blue" | "u" => new_color = new_color.union(ColorSet::BLUE),
303 "black" | "b" => new_color = new_color.union(ColorSet::BLACK),
304 "red" | "r" => new_color = new_color.union(ColorSet::RED),
305 "green" | "g" => new_color = new_color.union(ColorSet::GREEN),
306 _ => {}
307 }
308 }
309 if let Some(ts) = effect_ts {
310 perpetual_colors::PerpetualColors {
311 timestamp: ts,
312 colors: new_color,
313 overwrite: true,
314 }
315 .apply_effect(ctx.game.card_mut(card_id));
316 } else {
317 ctx.game.card_mut(card_id).set_color(new_color);
318 }
319 }
320 }
321
322 if let Some(action) = sa.ir.at_eot.as_deref() {
325 let remembered: Vec<crate::ids::CardId> = eot_targets
326 .iter()
327 .copied()
328 .filter(|&cid| ctx.game.card(cid).zone == ZoneType::Battlefield)
329 .collect();
330 crate::ability::spell_ability_effect::register_at_eot(
331 ctx.trigger_handler,
332 ctx.game,
333 sa,
334 action,
335 remembered,
336 );
337 }
338}
339
340fn resolve_animate_targets(
342 ctx: &mut EffectContext,
343 sa: &SpellAbility,
344 _controller: crate::ids::PlayerId,
345) -> Vec<crate::ids::CardId> {
346 if let Some(target) = sa.target_chosen.target_card {
348 return vec![target];
349 }
350
351 if let Some(defined) = sa.defined_ref() {
353 match defined {
354 DefinedRef::SelfCard => {
355 if let Some(src) = sa.source {
356 return vec![src];
357 }
358 }
359 DefinedRef::ParentTarget => {
360 if let Some(pt) = ctx.parent_target_card {
361 return vec![pt];
362 }
363 }
364 DefinedRef::Remembered => {
365 if let Some(src) = sa.source {
366 return ctx.game.card(src).remembered_cards.clone();
367 }
368 }
369 _ => {}
370 }
371 }
372
373 if let Some(src) = sa.source {
375 vec![src]
376 } else {
377 vec![]
378 }
379}