manabrew_engine/ability/effects/
clone_effect.rs1use forge_foundation::{CardTypeLine, ColorSet, ZoneType};
2
3use super::{matches_valid_cards_for_sa, EffectContext};
4use crate::parsing::split_param_list_value;
5use crate::spellability::SpellAbility;
6
7#[manabrew_engine_macros::spell_effect(CloneEffect)]
21fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
22 let source_id = match sa.source {
23 Some(id) => id,
24 None => return,
25 };
26
27 let controller = sa.activating_player;
28
29 let clone_source = resolve_clone_source(ctx, sa, controller);
31 let clone_source_id = match clone_source {
32 Some(id) => id,
33 None => return,
34 };
35
36 let clone_target_id = if let Some(defined) = sa.ir.clone_target.as_deref() {
38 match defined {
39 "Self" => source_id,
40 "ParentTarget" => ctx.parent_target_card.unwrap_or(source_id),
41 "Remembered" => ctx
42 .game
43 .card(source_id)
44 .remembered_cards
45 .first()
46 .copied()
47 .unwrap_or(source_id),
48 _ => source_id,
49 }
50 } else {
51 source_id
53 };
54
55 let src = ctx.game.card(clone_source_id).clone();
57 let duration = crate::parsing::raw_get(&sa.ability_text, crate::parsing::keys::DURATION);
58 let active_animation = capture_active_animation(ctx.game.card(clone_target_id));
59 if ctx.game.card(clone_target_id).clone_state.is_none() {
60 let mut state = ctx.game.card(clone_target_id).capture_clone_state();
61 state.expires_at_cleanup = duration.is_some() || sa.ir.duration.is_some();
62 if let Some(animate_state) = ctx.game.card(clone_target_id).animate_state.as_ref() {
63 state.original_type_line = animate_state.original_type_line.clone();
64 state.original_base_power = animate_state.original_base_power;
65 state.original_base_toughness = animate_state.original_base_toughness;
66 state.original_color = animate_state.original_color;
67 }
68 ctx.game
69 .card_mut(clone_target_id)
70 .set_clone_state(Some(state));
71 }
72 let target = &mut ctx.game.cards[clone_target_id.index()];
73 crate::card::card_copy_service::copy_copiable_characteristics(&src, target);
74 target.add_clone_state();
75 target.activated_abilities = src.activated_abilities.clone();
76 target.static_abilities = src.static_abilities.clone();
77 target.replacement_effects = src.replacement_effects.clone();
78 target.ensure_crew_activated_ability();
79 target.base_ability_count = target.activated_abilities.len();
80 target.base_trigger_count = target.triggers.len();
81 target.set_perpetual(&src, false);
82 target.reset_changed_card_traits_baseline_to_current();
83
84 if let Some(add_types) = sa.ir.add_types.as_deref() {
86 for ty in split_param_list_value(Some(add_types), " & ") {
87 ctx.game.card_mut(clone_target_id).add_type(&ty);
88 }
89 }
90
91 if let Some(set_color) = sa.ir.set_color.as_deref() {
92 ctx.game
93 .card_mut(clone_target_id)
94 .set_color(ColorSet::from_names(set_color));
95 }
96
97 if let Some(power) = sa
98 .ir
99 .set_power
100 .as_deref()
101 .and_then(|value| value.parse().ok())
102 {
103 ctx.game
104 .card_mut(clone_target_id)
105 .set_base_power(Some(power));
106 }
107 if let Some(toughness) = sa
108 .ir
109 .set_toughness
110 .as_deref()
111 .and_then(|value| value.parse().ok())
112 {
113 ctx.game
114 .card_mut(clone_target_id)
115 .set_base_toughness(Some(toughness));
116 }
117
118 if let Some(add_kws) = sa.ir.add_keywords.as_deref() {
119 let keywords = add_kws.strip_prefix("IfNew ").unwrap_or(add_kws);
120 for kw in split_param_list_value(Some(keywords), " & ") {
121 ctx.game
122 .card_mut(clone_target_id)
123 .add_intrinsic_keyword(&kw);
124 }
125 }
126
127 if let Some(animation) = active_animation {
128 reapply_active_animation(ctx.game.card_mut(clone_target_id), &animation);
129 }
130
131 if let Some(pump_kws) = sa.ir.pump_keywords.as_deref() {
133 for kw in split_param_list_value(Some(pump_kws), " & ") {
134 ctx.game
135 .card_mut(clone_target_id)
136 .add_intrinsic_keyword(&kw);
137 }
138 }
139
140 ctx.trigger_handler
142 .register_active_trigger(ctx.game, clone_target_id);
143}
144
145pub fn run(game: &mut crate::game::GameState, card_id: crate::ids::CardId) {
152 if game.card(card_id).zone != ZoneType::Battlefield {
153 return;
154 }
155 let card = game.card_mut(card_id);
158 card.remove_clone_state();
159 card.imprinted_cards.clear();
160 card.remembered_cards.clear();
161}
162
163fn resolve_clone_source(
165 ctx: &mut EffectContext,
166 sa: &SpellAbility,
167 controller: crate::ids::PlayerId,
168) -> Option<crate::ids::CardId> {
169 if let Some(target) = sa.target_chosen.target_card {
171 return Some(target);
172 }
173
174 if let Some(defined) = sa.defined() {
176 match defined {
177 "Remembered" => {
178 if let Some(src) = sa.source {
179 return ctx.game.card(src).remembered_cards.first().copied();
180 }
181 }
182 "ParentTarget" => {
183 return ctx.parent_target_card;
184 }
185 _ => {}
186 }
187 }
188
189 if let Some(filter) = sa.ir.choices.as_deref().map(str::to_string) {
191 let filter_selector = sa.ir.choices_selector.as_ref();
192 let zone = sa.ir.choice_zone.unwrap_or(ZoneType::Battlefield);
193
194 let mut valid = Vec::new();
195 for &pid in &ctx.game.player_order.clone() {
196 let zone_cards = ctx.game.cards_in_zone(zone, pid).to_vec();
197 for cid in zone_cards {
198 if matches_valid_cards_for_sa(
199 ctx.game,
200 sa,
201 ctx.game.card(cid),
202 filter_selector,
203 &filter,
204 ) {
205 valid.push(cid);
206 }
207 }
208 }
209
210 if valid.is_empty() {
211 return None;
212 }
213
214 ctx.agents[controller.index()].snapshot_state(ctx.game, ctx.mana_pools);
215 let chosen =
216 ctx.agents[controller.index()].choose_cards_for_effect(controller, &valid, 1, 1);
217 return chosen.first().copied();
218 }
219
220 None
221}
222
223#[derive(Clone)]
224struct ActiveAnimationSnapshot {
225 original_type_line: CardTypeLine,
226 original_color: ColorSet,
227 original_base_power: Option<i32>,
228 original_base_toughness: Option<i32>,
229 original_keywords: Option<Vec<String>>,
230 type_line: CardTypeLine,
231 color: ColorSet,
232 base_power: Option<i32>,
233 base_toughness: Option<i32>,
234 keywords: Vec<String>,
235}
236
237fn capture_active_animation(card: &crate::card::Card) -> Option<ActiveAnimationSnapshot> {
238 let state = card.animate_state.as_ref()?;
239 Some(ActiveAnimationSnapshot {
240 original_type_line: state.original_type_line.clone(),
241 original_color: state.original_color,
242 original_base_power: state.original_base_power,
243 original_base_toughness: state.original_base_toughness,
244 original_keywords: state
245 .original_keywords
246 .as_ref()
247 .map(|kws| kws.iter_strings().map(str::to_string).collect()),
248 type_line: card.type_line.clone(),
249 color: card.color,
250 base_power: card.base_power,
251 base_toughness: card.base_toughness,
252 keywords: card.keywords.iter_strings().map(str::to_string).collect(),
253 })
254}
255
256fn reapply_active_animation(card: &mut crate::card::Card, animation: &ActiveAnimationSnapshot) {
257 for supertype in animation
258 .type_line
259 .supertypes
260 .difference(&animation.original_type_line.supertypes)
261 {
262 card.type_line.supertypes.insert(*supertype);
263 }
264 for core_type in animation
265 .type_line
266 .core_types
267 .difference(&animation.original_type_line.core_types)
268 {
269 card.type_line.core_types.insert(*core_type);
270 }
271 for subtype in &animation.type_line.subtypes {
272 if animation
273 .original_type_line
274 .subtypes
275 .iter()
276 .any(|original| original.eq_ignore_ascii_case(subtype))
277 {
278 continue;
279 }
280 if !card
281 .type_line
282 .subtypes
283 .iter()
284 .any(|existing| existing.eq_ignore_ascii_case(subtype))
285 {
286 card.type_line.subtypes.push(subtype.clone());
287 }
288 }
289 card.update_types();
290 card.update_types_for_view();
291
292 if animation.color != animation.original_color {
293 card.color = animation.color;
294 }
295 if animation.base_power != animation.original_base_power {
296 card.base_power = animation.base_power;
297 }
298 if animation.base_toughness != animation.original_base_toughness {
299 card.base_toughness = animation.base_toughness;
300 }
301
302 let original_keywords = animation.original_keywords.as_deref().unwrap_or(&[]);
303 for keyword in &animation.keywords {
304 if original_keywords
305 .iter()
306 .any(|original| original.eq_ignore_ascii_case(keyword))
307 {
308 continue;
309 }
310 if !card
311 .keywords
312 .iter_strings()
313 .any(|existing| existing.eq_ignore_ascii_case(keyword))
314 {
315 card.add_intrinsic_keyword(keyword);
316 }
317 }
318}