manabrew_engine/replacement/
replace_moved.rs1use std::collections::HashMap;
6
7use forge_foundation::ZoneType;
8
9use crate::ability::effects::{self, EffectContext};
10use crate::agent::{PassAgent, PlayerAgent};
11use crate::card::Card;
12use crate::game::GameState;
13use crate::game_rng::ThreadRngAdapter;
14use crate::ids::CardId;
15use crate::mana::ManaPool;
16use crate::spellability::build_spell_ability;
17use crate::trigger::TriggerHandler;
18
19use super::replacement_effect::{zone_matches, ReplacementEffect};
20use super::replacement_handler::{ReplacementEvent, ReplacementRuntime};
21use super::replacement_result::ReplacementResult;
22use super::replacement_type::ReplacementType;
23use crate::card_trait_base::CardTrait;
24
25pub fn can_replace(
27 effect: &ReplacementEffect,
28 event: &ReplacementEvent,
29 game: &GameState,
30 source_card: &Card,
31) -> bool {
32 if effect.event != ReplacementType::Moved {
33 return false;
34 }
35 let (moving_id, origin, destination, is_discard) = match event {
36 ReplacementEvent::Moved {
37 card,
38 origin,
39 destination,
40 is_discard,
41 } => (*card, *origin, *destination, *is_discard),
42 _ => return false,
43 };
44 if let Some(requires_discard) = effect.ir.discard {
47 if requires_discard != is_discard {
48 return false;
49 }
50 }
51 if let Some(dest) = effect.ir.destination_text.as_deref() {
52 if !zone_matches(dest, destination) {
53 return false;
54 }
55 }
56 if let Some(exclude) = effect.ir.exclude_destination_text.as_deref() {
57 if zone_matches(exclude, destination) {
58 return false;
59 }
60 }
61 if let Some(orig) = effect.ir.origin_text.as_deref() {
62 if !zone_matches(orig, origin) {
63 return false;
64 }
65 }
66 let moving_card = &game.cards[moving_id.index()];
67 if let Some(valid) = effect.ir.valid_card_selector.as_ref() {
68 if !effect.matches_compiled_valid_card(valid, moving_card, source_card) {
69 return false;
70 }
71 }
72 if effect.ir.flashback_cast == Some(true) && !moving_card.cast_with_flashback {
74 return false;
75 }
76 if effect.ir.harmonize_cast == Some(true) && !moving_card.cast_with_harmonize {
78 return false;
79 }
80 if let Some(valid_lki) = effect.ir.valid_lki_text.as_deref() {
81 if !effect.matches_valid_card(valid_lki, moving_card, source_card) {
82 return false;
83 }
84 }
85 if destination == ZoneType::Battlefield && !effect.can_replace_etb(source_card, moving_card) {
87 return false;
88 }
89 true
90}
91
92pub fn execute(
94 effect: &ReplacementEffect,
95 event: &mut ReplacementEvent,
96 game: &mut GameState,
97 source_card_id: CardId,
98 agents: Option<&mut [Box<dyn PlayerAgent>]>,
99 runtime: Option<&mut ReplacementRuntime<'_>>,
100) -> ReplacementResult {
101 let (moving_id, _destination) = match event {
102 ReplacementEvent::Moved {
103 card, destination, ..
104 } => (*card, destination),
105 _ => return ReplacementResult::NotReplaced,
106 };
107 let redirect = effect
110 .ir
111 .new_destination_text
112 .as_deref()
113 .or(effect.replace_with());
114
115 if let Some(new_dest) = redirect {
116 let new_zone = match new_dest.trim() {
117 "Exile" => Some(ZoneType::Exile),
118 "Graveyard" => Some(ZoneType::Graveyard),
119 "Hand" => Some(ZoneType::Hand),
120 "Library" => Some(ZoneType::Library),
121 "Battlefield" => Some(ZoneType::Battlefield),
122 "Command" => Some(ZoneType::Command),
123 _ => None,
124 };
125 if let Some(z) = new_zone {
126 if let ReplacementEvent::Moved { destination, .. } = event {
127 *destination = z;
128 }
129 if z == ZoneType::Exile && effect.ir.exiled_with_effect_source {
130 let exile_source = game
131 .card(source_card_id)
132 .effect_source
133 .unwrap_or(source_card_id);
134 game.card_mut(moving_id).set_exiled_by(Some(exile_source));
135 game.card_mut(exile_source).add_remembered_card(moving_id);
136 }
137 return ReplacementResult::Updated;
138 }
139 }
140 if let Some(replace_with_key) = effect.replace_with() {
142 let succeeded = execute_replace_with(
143 effect,
144 replace_with_key,
145 game,
146 source_card_id,
147 event,
148 agents,
149 runtime,
150 );
151 if !succeeded {
152 return ReplacementResult::NotReplaced;
153 }
154 }
155 if let Some(result) = effect.ir.replacement_result.as_deref() {
156 return match result {
157 "Updated" => ReplacementResult::Updated,
158 "Replaced" => ReplacementResult::Replaced,
159 "Skipped" => ReplacementResult::Skipped,
160 "Prevented" => ReplacementResult::Prevented,
161 _ => ReplacementResult::Replaced,
162 };
163 }
164 ReplacementResult::Replaced
165}
166
167fn execute_replace_with(
168 effect: &ReplacementEffect,
169 replace_with: &str,
170 game: &mut GameState,
171 source_card_id: CardId,
172 event: &ReplacementEvent,
173 agents: Option<&mut [Box<dyn PlayerAgent>]>,
174 mut runtime: Option<&mut ReplacementRuntime<'_>>,
175) -> bool {
176 let Some(raw) = game.card(source_card_id).svars.get(replace_with).cloned() else {
177 return false;
178 };
179 let controller = game.card(source_card_id).controller;
180 let mut sa = build_spell_ability(game, source_card_id, &raw, controller);
181 effect.set_replacing_objects(event, &mut sa);
182
183 #[allow(unused_assignments)]
186 let mut local_agents_storage: Option<Vec<Box<dyn PlayerAgent>>> = None;
187 let agents: &mut [Box<dyn PlayerAgent>] = if let Some(agents) = agents {
188 agents
189 } else {
190 local_agents_storage = Some(
191 (0..game.players.len())
192 .map(|_| Box::new(PassAgent) as Box<dyn PlayerAgent>)
193 .collect(),
194 );
195 local_agents_storage.as_mut().unwrap().as_mut_slice()
196 };
197
198 let mut local_mana_pools: Vec<ManaPool> =
199 (0..game.players.len()).map(|_| ManaPool::new()).collect();
200 let mana_pools_for_targets: &[ManaPool] = if let Some(rt) = runtime.as_ref() {
201 rt.mana_pools.as_slice()
202 } else {
203 local_mana_pools.as_slice()
204 };
205 if sa.uses_targeting() && !sa.setup_targets(game, agents, mana_pools_for_targets) {
206 return false;
207 }
208
209 let mut local_trigger_handler = TriggerHandler::new();
210 let local_token_templates: HashMap<String, Card> = HashMap::new();
211 let local_token_art_variants: HashMap<(String, String), usize> = HashMap::new();
212 let local_token_fallback: HashMap<String, String> = HashMap::new();
213 let local_edition_dates: HashMap<String, String> = HashMap::new();
214 let mut local_rng = ThreadRngAdapter;
215
216 let mut parent_target_card: Option<CardId> = None;
217 let mut parent_target_player = None;
218 let mut current_sa: Option<&crate::spellability::SpellAbility> = Some(&sa);
219 while let Some(cur) = current_sa {
220 let mut sa_with_ctx;
221 let sa_ref = if parent_target_player.is_some() && cur.target_chosen.target_player.is_none()
222 {
223 sa_with_ctx = cur.clone();
224 sa_with_ctx.target_chosen.target_player = parent_target_player;
225 &sa_with_ctx
226 } else {
227 cur
228 };
229
230 let (
231 trigger_handler_ref,
232 token_templates_ref,
233 token_art_ref,
234 token_fb_ref,
235 edition_dates_ref,
236 mana_pools_ref,
237 rng_ref,
238 ): (
239 &mut TriggerHandler,
240 &HashMap<String, Card>,
241 &HashMap<(String, String), usize>,
242 &HashMap<String, String>,
243 &HashMap<String, String>,
244 &mut Vec<ManaPool>,
245 &mut dyn crate::game_rng::GameRng,
246 ) = if let Some(rt) = runtime.as_deref_mut() {
247 (
248 rt.trigger_handler,
249 rt.token_templates,
250 rt.token_art_variants,
251 rt.token_fallback,
252 rt.edition_dates,
253 rt.mana_pools,
254 rt.rng,
255 )
256 } else {
257 (
258 &mut local_trigger_handler,
259 &local_token_templates,
260 &local_token_art_variants,
261 &local_token_fallback,
262 &local_edition_dates,
263 &mut local_mana_pools,
264 &mut local_rng,
265 )
266 };
267
268 let mut ctx = EffectContext {
269 game,
270 combat: None,
271 agents,
272 trigger_handler: trigger_handler_ref,
273 token_templates: token_templates_ref,
274 token_art_variants: token_art_ref,
275 token_fallback: token_fb_ref,
276 edition_dates: edition_dates_ref,
277 mana_pools: mana_pools_ref,
278 parent_target_card,
279 rng: rng_ref,
280 };
281 effects::resolve_effect(&mut ctx, sa_ref);
282 parent_target_card = sa_ref.target_chosen.target_card;
283 parent_target_player = sa_ref.target_chosen.target_player;
284 current_sa = cur.get_sub_ability();
285 }
286 true
287}
288
289