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