1use crate::agent::GameEntity;
6use crate::card::valid_filter;
7use crate::card::Card;
8use crate::game::GameState;
9use crate::ids::CardId;
10use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
11
12use super::replacement_effect::ReplacementEffect;
13use super::replacement_handler::ReplacementEvent;
14use super::replacement_result::ReplacementResult;
15use super::replacement_type::ReplacementType;
16use crate::card_trait_base::CardTrait;
17
18pub fn has_any_in_counter_map(effect: &ReplacementEffect, counter_type: Option<&str>) -> bool {
26 match effect.ir.valid_counter_type_text.as_deref() {
27 None => true, Some(valid) => match counter_type {
29 None => false, Some(ct) => valid.split(',').any(|v| v.trim().eq_ignore_ascii_case(ct)),
31 },
32 }
33}
34
35pub fn mode_check(effect: &ReplacementEffect, event: &ReplacementType) -> bool {
42 match event {
43 ReplacementType::AddCounter => true,
44 ReplacementType::Moved => effect.ir.counter_map,
45 _ => false,
46 }
47}
48
49pub fn can_replace(
51 effect: &ReplacementEffect,
52 event: &ReplacementEvent,
53 game: &GameState,
54 source_card: &Card,
55) -> bool {
56 if effect.event != ReplacementType::AddCounter {
57 return false;
58 }
59 let (target, counter_map, cause, is_effect, etb) = match event {
60 ReplacementEvent::AddCounter {
61 target,
62 counter_map,
63 cause,
64 is_effect,
65 ..
66 } => (*target, counter_map, cause.as_deref(), *is_effect, false),
67 ReplacementEvent::Moved {
68 card,
69 destination: ZoneType::Battlefield,
70 counter_map: Some(counter_map),
71 counter_cause,
72 counter_is_effect,
73 ..
74 } => (
75 GameEntity::Card(*card),
76 counter_map,
77 counter_cause.as_deref(),
78 *counter_is_effect,
79 true,
80 ),
81 _ => return false,
82 };
83 if effect.ir.effect_only && !is_effect {
84 return false;
85 }
86 if let Some(valid) = effect.ir.valid_card_selector.as_ref() {
87 let GameEntity::Card(target) = target else {
88 return false;
89 };
90 let mut target_card = game.card(target).clone();
91 if etb {
92 target_card.zone = ZoneType::Battlefield;
93 }
94 if !effect.matches_compiled_valid_card(valid, &target_card, source_card) {
95 return false;
96 }
97 }
98 if let Some(valid) = effect.ir.valid_player_selector.as_ref() {
99 let GameEntity::Player(target) = target else {
100 return false;
101 };
102 if !effect.matches_compiled_valid_player(valid, target, source_card) {
103 return false;
104 }
105 }
106 if let Some(valid) = effect.ir.valid_object_selector.as_ref() {
107 let matches = match target {
108 GameEntity::Card(target) => {
109 let mut target_card = game.card(target).clone();
110 if etb {
111 target_card.zone = ZoneType::Battlefield;
112 }
113 effect.matches_compiled_valid_card(valid, &target_card, source_card)
114 }
115 GameEntity::Player(target) => {
116 effect.matches_compiled_valid_player(valid, target, source_card)
117 }
118 };
119 if !matches {
120 return false;
121 }
122 }
123 if etb
124 && effect.base().host_card_id()
125 == match target {
126 GameEntity::Card(card) => card,
127 GameEntity::Player(_) => return false,
128 }
129 && !effect
130 .ir
131 .valid_card_text
132 .as_deref()
133 .is_some_and(|valid| valid.starts_with("Card.Self"))
134 {
135 return false;
136 }
137 if let Some(valid) = effect.ir.valid_cause_text.as_deref() {
138 let Some(cause) = cause else {
139 return false;
140 };
141 let ability_host = cause.source.map(|card| game.card(card));
142 if !crate::spellability::matches_valid_sa(valid, cause, source_card, ability_host) {
143 return false;
144 }
145 }
146 if !counter_map.iter().any(|entry| {
147 let source_matches = effect
148 .ir
149 .valid_source_selector
150 .as_ref()
151 .is_none_or(|valid| {
152 entry.source.is_some_and(|source| {
153 valid_filter::matches_valid_player_selector(
154 valid,
155 source,
156 source_card.controller,
157 )
158 })
159 });
160 let type_matches = effect
161 .ir
162 .valid_counter_type_text
163 .as_deref()
164 .is_none_or(|valid| {
165 let expected = crate::ability::effects::parse_counter_type(valid);
166 entry.counters.contains_key(&expected)
167 });
168 source_matches && type_matches
169 }) {
170 return false;
171 }
172 true
173}
174
175pub fn execute(
177 effect: &ReplacementEffect,
178 event: &mut ReplacementEvent,
179 _game: &mut GameState,
180 _source_card_id: CardId,
181 agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
182) -> ReplacementResult {
183 let etb = matches!(event, ReplacementEvent::Moved { .. });
184 let (target, cause, is_effect, counter_map, after_replacement_static_abilities) = match event {
185 ReplacementEvent::AddCounter {
186 target,
187 cause,
188 is_effect,
189 counter_map,
190 after_replacement_static_abilities,
191 ..
192 } => (
193 *target,
194 cause.clone(),
195 *is_effect,
196 counter_map,
197 after_replacement_static_abilities,
198 ),
199 ReplacementEvent::Moved {
200 card,
201 counter_map: Some(counter_map),
202 counter_cause,
203 counter_is_effect,
204 after_replacement_static_abilities,
205 ..
206 } => (
207 GameEntity::Card(*card),
208 counter_cause.clone(),
209 *counter_is_effect,
210 counter_map,
211 after_replacement_static_abilities,
212 ),
213 _ => return ReplacementResult::NotReplaced,
214 };
215 if let Some(replace) = effect.replace_with() {
216 match replace {
217 "AddOneMoreCounter" | "AddOneMoreCounters" => {
218 update_matching_counters(
219 effect,
220 counter_map,
221 _game.card(_source_card_id).controller,
222 |amount| amount + 1,
223 );
224 return ReplacementResult::Updated;
225 }
226 "AddTwiceCounters" | "DoubleCounters" => {
227 update_matching_counters(
228 effect,
229 counter_map,
230 _game.card(_source_card_id).controller,
231 |amount| amount * 2,
232 );
233 return ReplacementResult::Updated;
234 }
235 _ => {
236 let original = counter_map.clone();
237 let selected_sources =
238 selected_counter_sources(effect, &original, target, _game, agents);
239 for entry in original {
240 for (counter_type, amount) in entry.counters {
241 if !entry_matches(
242 effect,
243 entry.source,
244 &counter_type,
245 _game.card(_source_card_id).controller,
246 ) {
247 continue;
248 }
249 if selected_sources.iter().any(|(selected_type, source)| {
250 *selected_type == counter_type && *source != entry.source
251 }) {
252 continue;
253 }
254 let mut single = ReplacementEvent::AddCounter {
255 target,
256 source: entry.source,
257 counter_type: counter_type.clone(),
258 count: amount,
259 counter_map: Vec::new(),
260 after_replacement_static_abilities: Vec::new(),
261 cause: cause.clone(),
262 is_effect,
263 };
264 if super::replacement_handler::execute_replace_with_numeric_update(
265 effect,
266 &mut single,
267 _game,
268 _source_card_id,
269 "CounterNum",
270 )
271 .is_some()
272 {
273 let ReplacementEvent::AddCounter { count, .. } = single else {
274 unreachable!();
275 };
276 if let Some(target_entry) = counter_map
277 .iter_mut()
278 .find(|target_entry| target_entry.source == entry.source)
279 {
280 target_entry.counters.insert(counter_type, count);
281 }
282 }
283 }
284 }
285 if let Some(
286 crate::replacement::replacement_effect::ReplacementChainIr::ReplaceCounter {
287 after_replacement_static_abilities: static_abilities,
288 ..
289 },
290 ) = crate::replacement::replacement_effect::resolve_replace_with_chain(
291 effect,
292 _game.card(_source_card_id),
293 ) {
294 if !static_abilities.is_empty() {
295 after_replacement_static_abilities
296 .push((_source_card_id, static_abilities));
297 }
298 }
299 return ReplacementResult::Updated;
300 }
301 }
302 }
303 if etb {
304 counter_map.clear();
305 return ReplacementResult::Updated;
306 }
307 ReplacementResult::Replaced
308}
309
310fn selected_counter_sources(
311 effect: &ReplacementEffect,
312 counter_map: &[super::replacement_handler::CounterMapValue],
313 target: GameEntity,
314 game: &GameState,
315 mut agents: Option<&mut [Box<dyn crate::agent::PlayerAgent>]>,
316) -> Vec<(crate::card::CounterType, Option<crate::ids::PlayerId>)> {
317 let choose_counter = matches!(
318 crate::replacement::replacement_effect::resolve_replace_with_chain(
319 effect,
320 game.card(effect.base().host_card_id()),
321 ),
322 Some(
323 crate::replacement::replacement_effect::ReplacementChainIr::ReplaceCounter {
324 choose_counter: true,
325 ..
326 }
327 )
328 );
329 if !choose_counter || counter_map.len() <= 1 {
330 return Vec::new();
331 }
332 let chooser = match target {
333 GameEntity::Card(card) => game.card(card).controller,
334 GameEntity::Player(player) => player,
335 };
336 let mut counter_types = Vec::new();
337 for entry in counter_map {
338 for counter_type in entry.counters.keys() {
339 if !counter_types.contains(counter_type) {
340 counter_types.push(counter_type.clone());
341 }
342 }
343 }
344 counter_types
345 .into_iter()
346 .filter_map(|counter_type| {
347 let sources: Vec<_> = counter_map
348 .iter()
349 .filter(|entry| entry.counters.contains_key(&counter_type))
350 .filter_map(|entry| entry.source)
351 .collect();
352 let source = if let Some(agents) = agents.as_deref_mut() {
353 agents[chooser.index()].choose_target_player(chooser, &sources, None)
354 } else {
355 sources.first().copied()
356 };
357 source.map(|source| (counter_type, Some(source)))
358 })
359 .collect()
360}
361
362pub(crate) fn apply_after_replacement_static_abilities(
363 game: &mut GameState,
364 source_card_id: CardId,
365 after_replacement_static_abilities: Vec<String>,
366) {
367 let source = game.card(source_card_id).clone();
368 let mut effect_card = Card::new(
369 CardId(0),
370 format!("{}'s Effect", source.card_name),
371 source.controller,
372 CardTypeLine::parse("Effect"),
373 ManaCost::parse("0"),
374 ColorSet::COLORLESS,
375 None,
376 None,
377 vec![],
378 vec![],
379 );
380 effect_card.set_controller(source.controller);
381 effect_card.set_effect_source(Some(source_card_id));
382 effect_card.set_temp_effect_until_eot(true);
383 for raw in after_replacement_static_abilities {
384 if let Some(mut static_ability) =
385 crate::staticability::parse_static_ability(&format!("S$ {raw}"))
386 {
387 static_ability.ir.active_zones = vec![ZoneType::Command];
388 static_ability.ir.has_zone_keys = true;
389 effect_card.add_static_ability(static_ability);
390 }
391 }
392 for (name, value) in source.svars {
393 effect_card.set_s_var_if_absent(name, value);
394 }
395 let effect_id = game.create_card(effect_card);
396 game.move_card(effect_id, ZoneType::Command, source.controller);
397}
398
399fn entry_matches(
400 effect: &ReplacementEffect,
401 source: Option<crate::ids::PlayerId>,
402 counter_type: &crate::card::CounterType,
403 source_controller: crate::ids::PlayerId,
404) -> bool {
405 let source_matches = effect
406 .ir
407 .valid_source_selector
408 .as_ref()
409 .is_none_or(|valid| {
410 source.is_some_and(|source| {
411 valid_filter::matches_valid_player_selector(valid, source, source_controller)
412 })
413 });
414 let type_matches = effect
415 .ir
416 .valid_counter_type_text
417 .as_deref()
418 .is_none_or(|valid| crate::ability::effects::parse_counter_type(valid) == *counter_type);
419 source_matches && type_matches
420}
421
422fn update_matching_counters(
423 effect: &ReplacementEffect,
424 counter_map: &mut [super::replacement_handler::CounterMapValue],
425 source_controller: crate::ids::PlayerId,
426 update: impl Fn(i32) -> i32,
427) {
428 for entry in counter_map {
429 for (counter_type, amount) in &mut entry.counters {
430 if entry_matches(effect, entry.source, counter_type, source_controller) {
431 *amount = update(*amount).max(0);
432 }
433 }
434 }
435}