Skip to main content

manabrew_engine/ability/effects/
clone_effect.rs

1use 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/// `SP$ Clone` — one card becomes a copy of another.
8///
9/// Mirrors Java's `CloneEffect.java`.
10///
11/// # Params
12/// - `Choices` — filter for valid clone sources (if player picks)
13/// - `ChoiceZone` — zone to pick from (default Battlefield)
14/// - `Defined$` — resolve defined cards as the clone source
15/// - `CloneTarget` — defined cards to be cloned onto (default: source card)
16/// - `PumpKeywords` — extra keywords on the copy
17/// Struct form of this effect so it can participate in the
18/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
19/// `CloneEffect` class extending `SpellAbilityEffect`.
20#[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    // Step 1: Determine the clone source (what to copy FROM)
30    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    // Step 2: Determine the clone target (what to copy ONTO)
37    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        // Default: the source card itself (the creature entering as a clone)
52        source_id
53    };
54
55    // Step 3: Copy characteristics from source → target
56    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    // Step 4: Apply clone-state modifications from the cloning ability.
85    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    // Step 5: Apply PumpKeywords$ (extra temporary keywords on the copy)
132    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    // Step 6: Re-register triggers for the cloned card
141    ctx.trigger_handler
142        .register_active_trigger(ctx.game, clone_target_id);
143}
144
145/// End-of-turn revert for clone effects. Mirrors the `GameCommand.run()`
146/// anonymous class in Java `CloneEffect` that calls `removeCloneState`,
147/// clears imprinted/remembered cards, and restores the original state.
148///
149/// Removes the clone stamp from the card, reverting copied characteristics
150/// and restoring original remembered/imprinted state.
151pub 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    // Revert copiable characteristics by clearing clone-specific state.
156    // The card's base characteristics (from the card definition) take over.
157    let card = game.card_mut(card_id);
158    card.remove_clone_state();
159    card.imprinted_cards.clear();
160    card.remembered_cards.clear();
161}
162
163/// Determine which card to copy FROM.
164fn resolve_clone_source(
165    ctx: &mut EffectContext,
166    sa: &SpellAbility,
167    controller: crate::ids::PlayerId,
168) -> Option<crate::ids::CardId> {
169    // Check explicit target first
170    if let Some(target) = sa.target_chosen.target_card {
171        return Some(target);
172    }
173
174    // Check Defined$
175    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    // Check Choices — player selects from valid cards
190    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}