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