Skip to main content

manabrew_engine/ability/effects/
change_targets_effect.rs

1//! ChangeTargets effect — redirect a spell or ability's targets.
2//!
3//! Ported from Java's `ChangeTargetsEffect.java`.
4//! Change the target(s) of target spell or ability.
5//! In Java this introspects the MagicStack to find the targeted spell's
6//! StackInstance and updates its TargetChoices. In our engine, the stack
7//! stores SpellAbility instances with target_chosen fields.
8
9use forge_foundation::ZoneType;
10
11use super::EffectContext;
12use crate::ability::ability_ir::DefinedRef;
13use crate::ids::CardId;
14use crate::parsing::keys;
15use crate::spellability::SpellAbility;
16
17/// Configure the spell ability during construction.
18/// Mirrors Java `ChangeTargetsEffect.buildSpellAbility` — sets the target zone
19/// to Stack so that the ability targets spells on the stack.
20pub fn build_spell_ability(sa: &mut SpellAbility) {
21    if sa.uses_targeting() {
22        if let Some(ref mut tr) = sa.target_restrictions {
23            tr.tgt_zone = vec![ZoneType::Stack];
24        }
25    }
26}
27
28/// Struct form of this effect so it can participate in the
29/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
30/// `ChangeTargetsEffect` class extending `SpellAbilityEffect`.
31#[manabrew_engine_macros::spell_effect(ChangeTargetsEffect)]
32fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
33    let controller = sa.activating_player;
34
35    // Find the targeted spell on the stack
36    let target_spell_card = match sa.target_chosen.target_card {
37        Some(card_id) => card_id,
38        None => return,
39    };
40
41    // Verify the target is on the stack
42    if ctx.game.card(target_spell_card).zone != ZoneType::Stack {
43        return;
44    }
45
46    // Handle RandomTarget mode: pick a random new legal target
47    if sa.param_is_true(keys::RANDOM_TARGET) {
48        // Find all creatures/permanents on battlefield as candidates
49        let candidates: Vec<CardId> = ctx
50            .game
51            .cards
52            .iter()
53            .filter(|c| c.zone == ZoneType::Battlefield)
54            .map(|c| c.id)
55            .collect();
56
57        if candidates.is_empty() {
58            return;
59        }
60
61        // Pick random new target
62        let idx = ctx.rng.next_int(candidates.len() as i32) as usize % candidates.len();
63        let new_target = candidates[idx];
64
65        // Update the spell's target on the stack
66        // In our simplified stack model, we update the card's svar to track new target
67        ctx.game
68            .card_mut(target_spell_card)
69            .set_s_var("RedirectedTarget", format!("{}", new_target.0));
70        return;
71    }
72
73    // Handle DefinedMagnet mode: redirect to a specific permanent
74    if let Some(magnet_def) = sa.ir.defined_magnet_text.as_deref() {
75        let magnet_ref = DefinedRef::parse(magnet_def);
76        let new_target = if matches!(magnet_ref, DefinedRef::SelfCard) {
77            sa.source
78        } else if matches!(magnet_ref, DefinedRef::ParentTarget) {
79            sa.target_chosen.target_card
80        } else {
81            sa.source
82                .and_then(|sid| ctx.game.card(sid).remembered_cards.first().copied())
83        };
84
85        if let Some(new_tgt) = new_target {
86            ctx.game
87                .card_mut(target_spell_card)
88                .set_s_var("RedirectedTarget", format!("{}", new_tgt.0));
89        }
90        return;
91    }
92
93    // Default mode: let player choose new targets
94    // In full implementation this would present the controller with legal target choices.
95    // Auto-mode: agent chooses from battlefield permanents
96    let candidates: Vec<CardId> = ctx
97        .game
98        .cards
99        .iter()
100        .filter(|c| c.zone == ZoneType::Battlefield && c.controller != controller)
101        .map(|c| c.id)
102        .collect();
103
104    if candidates.is_empty() {
105        return;
106    }
107
108    // Agent chooses new target
109    ctx.agents[controller.index()].snapshot_state(ctx.game, ctx.mana_pools);
110    if let Some(chosen) = ctx.agents[controller.index()].choose_single_card_for_zone_change(
111        ctx.game,
112        controller,
113        &candidates,
114        "Choose new target",
115        false,
116    ) {
117        ctx.game
118            .card_mut(target_spell_card)
119            .set_s_var("RedirectedTarget", format!("{}", chosen.0));
120    }
121}