1use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
15
16use crate::card::Card;
17use crate::ids::{CardId, PlayerId};
18use crate::replacement::replacement_effect::parse_replacement_effect;
19use crate::spellability::{build_spell_ability_from_host_card, SpellAbility};
20use crate::staticability::parse_static_ability;
21use crate::trigger::trigger::parse_trigger;
22
23use super::{resolve_defined_player, resolve_defined_players, EffectContext};
24use crate::ability::ability_utils;
25use crate::ability::spell_ability_effect::{check_valid_duration, SpellAbilityEffect};
26use crate::spellability::AbilityDuration;
27
28pub struct EffectEffect;
30
31impl SpellAbilityEffect for EffectEffect {
32 fn resolve(ctx: &mut EffectContext, sa: &SpellAbility) {
33 resolve_impl(ctx, sa);
34 }
35}
36
37fn resolve_impl(ctx: &mut EffectContext, sa: &SpellAbility) {
38 let Some(source_id) = sa.source else {
39 return;
40 };
41
42 let duration = sa.ir.duration.as_ref();
43 if !check_valid_duration(ctx.game, sa, duration) {
44 return;
45 }
46
47 let (host_name, host_remembered_cards, host_remembered_players) = {
48 let host = ctx.game.card(source_id);
49 (
50 host.card_name.clone(),
51 host.remembered_cards.clone(),
52 host.remembered_players.clone(),
53 )
54 };
55
56 let abilities = split_csv_value(sa.ir.abilities.as_deref());
57 let triggers = split_csv_value(sa.ir.triggers.as_deref());
58 let static_refs = split_csv_value(sa.ir.static_abilities.as_deref());
59 let replacement_refs = split_csv_value(sa.ir.replacement_effects.as_deref());
60
61 let effect_name = resolve_effect_name(sa, &host_name);
62 let effect_owner_defined = sa.ir.effect_owner_text.as_deref().unwrap_or("You");
63 let mut owners = resolve_defined_players(effect_owner_defined, sa.activating_player, ctx.game);
64
65 if sa.ir.unique {
67 owners.retain(|pid| !player_has_effect_named(ctx, *pid, &effect_name));
68 }
69 if owners.is_empty() {
70 return;
71 }
72
73 let mut remember_cards: Vec<CardId> = Vec::new();
75 let mut remember_players: Vec<PlayerId> = Vec::new();
76 let mut remember_lki_cards: Vec<Card> = Vec::new();
77 populate_remember_lists(
78 ctx,
79 sa,
80 &host_remembered_cards,
81 &host_remembered_players,
82 &mut remember_cards,
83 &mut remember_players,
84 &mut remember_lki_cards,
85 );
86
87 let needs_nonempty_remember = sa.ir.forget_on_moved_text.is_some()
90 || sa.ir.exile_on_moved
91 || sa.ir.forget_on_phased_in
92 || sa.ir.forget_counter;
93 if needs_nonempty_remember
94 && remember_cards.is_empty()
95 && remember_players.is_empty()
96 && remember_lki_cards.is_empty()
97 && (sa.ir.remember_objects.is_some()
98 || sa.ir.remember_spell.is_some()
99 || sa.ir.remember_lki.is_some())
100 {
101 return;
102 }
103
104 let parsed_static_abilities = static_refs
107 .iter()
108 .filter_map(|svar_name| ctx.game.card(source_id).get_s_var(svar_name))
109 .filter_map(|raw| {
110 let mut static_ability = parse_static_ability(&format!("S$ {}", raw))?;
111 static_ability.ir.active_zones = vec![ZoneType::Command];
112 static_ability.ir.has_zone_keys = true;
113 static_ability.base.set_intrinsic(true);
114 Some(static_ability)
115 })
116 .collect::<Vec<_>>();
117
118 let trigger_svars = collect_svar_texts(ctx, source_id, &triggers);
121 let replacement_svars = collect_svar_texts(ctx, source_id, &replacement_refs);
122 let ability_svars = collect_svar_texts(ctx, source_id, &abilities);
123
124 let imprint_cards: Vec<CardId> = sa
126 .ir
127 .imprint_cards
128 .as_deref()
129 .map(|defined| {
130 ability_utils::get_defined_cards(
131 ctx.game,
132 sa.source,
133 defined,
134 Some(sa.activating_player),
135 )
136 })
137 .unwrap_or_default();
138
139 let chosen_snapshot = ChosenSnapshot::capture(ctx.game.card(source_id));
141
142 let chosen_number_override = sa
143 .ir
144 .set_chosen_number
145 .as_deref()
146 .map(ability_utils::calculate_amount);
147
148 for owner in owners {
149 let mut effect = Card::new(
150 CardId(0),
151 effect_name.clone(),
152 owner,
153 CardTypeLine::parse("Effect"),
154 ManaCost::parse("0"),
155 ColorSet::COLORLESS,
156 None,
157 None,
158 vec![],
159 vec![],
160 );
161 effect.set_controller(owner);
162 effect.set_effect_source(Some(source_id));
163 effect.set_static_abilities(parsed_static_abilities.clone());
164
165 for svar_text in &trigger_svars {
168 let mut next_id = 1000u32;
169 if let Some(mut trig) = parse_trigger(svar_text, &mut next_id) {
170 trig.set_active_zone(vec![ZoneType::Command]);
171 trig.set_intrinsic(true);
172 effect.add_trigger(trig);
173 }
174 }
175 for svar_text in &replacement_svars {
176 if let Some(mut re) = parse_replacement_effect(svar_text) {
177 re.base.set_active_zone(vec![ZoneType::Command]);
178 re.base.card_trait_base.set_intrinsic(true);
179 effect.add_replacement_effect(re);
180 }
181 }
182 for ability_text in &ability_svars {
183 let sa_granted = build_spell_ability_from_host_card(&effect, ability_text, owner);
184 effect.add_spell_ability(&sa_granted);
185 }
186
187 let host_svars = ctx.game.card(source_id).svars.clone();
189 for (k, v) in host_svars {
190 effect.set_s_var_if_absent(k, v);
191 }
192
193 apply_duration_flags(&mut effect, duration, source_id);
194 apply_forget_on_moved_flags(&mut effect, sa);
195 effect.add_remembered_cards(remember_cards.iter().copied());
196 effect.add_remembered_players(remember_players.iter().copied());
197 effect.remembered_lki_cards = remember_lki_cards.clone();
198 for imp in &imprint_cards {
199 effect.add_imprinted_card(*imp);
200 }
201 apply_chosen_state(&mut effect, &chosen_snapshot, chosen_number_override);
202 if sa.is_intrinsic() {
203 let host_clone = ctx.game.card(source_id).clone();
204 effect.copy_changed_text_from(&host_clone);
205 }
206
207 let effect_id = ctx.game.create_card(effect);
208 ctx.move_card(effect_id, ZoneType::Command, owner);
209 }
210
211 if let Some(action) = sa.ir.at_eot.as_deref() {
215 crate::ability::spell_ability_effect::register_at_eot(
216 ctx.trigger_handler,
217 ctx.game,
218 sa,
219 action,
220 vec![source_id],
221 );
222 }
223}
224
225fn split_csv_value(value: Option<&str>) -> Vec<String> {
226 value
227 .map(|v| {
228 v.split(',')
229 .map(str::trim)
230 .filter(|s| !s.is_empty())
231 .map(str::to_string)
232 .collect()
233 })
234 .unwrap_or_default()
235}
236
237fn resolve_effect_name(sa: &SpellAbility, host_name: &str) -> String {
238 if let Some(name) = sa.ir.name_text.as_deref() {
239 return name.to_string();
240 }
241 let suffix = if sa.ir.boon { "'s Boon" } else { "'s Effect" };
242 format!("{}{}", host_name, suffix)
243}
244
245fn player_has_effect_named(ctx: &EffectContext, player: PlayerId, name: &str) -> bool {
246 ctx.game
247 .cards_in_zone(ZoneType::Command, player)
248 .iter()
249 .any(|cid| ctx.game.card(*cid).card_name == name)
250}
251
252fn collect_svar_texts(ctx: &EffectContext, source_id: CardId, names: &[String]) -> Vec<String> {
253 let host = ctx.game.card(source_id);
254 names
255 .iter()
256 .filter_map(|n| host.get_s_var(n).map(str::to_string))
257 .collect()
258}
259
260fn populate_remember_lists(
261 ctx: &EffectContext,
262 sa: &SpellAbility,
263 host_remembered_cards: &[CardId],
264 host_remembered_players: &[PlayerId],
265 out_cards: &mut Vec<CardId>,
266 out_players: &mut Vec<PlayerId>,
267 out_lki_cards: &mut Vec<Card>,
268) {
269 if let Some(remember) = sa.ir.remember_objects.as_deref() {
270 for token in remember.split(',').map(str::trim).filter(|s| !s.is_empty()) {
271 match token {
272 "Remembered" => {
273 out_cards.extend(host_remembered_cards.iter().copied());
274 out_players.extend(host_remembered_players.iter().copied());
275 }
276 "RememberedCard" => out_cards.extend(host_remembered_cards.iter().copied()),
277 "RememberedPlayer" => out_players.extend(host_remembered_players.iter().copied()),
278 "RememberedLKI" => {
279 for &cid in host_remembered_cards {
280 out_lki_cards.push(crate::card::card_copy_service::get_lki_copy(
281 ctx.game.card(cid),
282 ));
283 }
284 }
285 "Targeted" => {
286 if let Some(cid) = sa.target_chosen.target_card.or(ctx.parent_target_card) {
287 out_cards.push(cid);
288 }
289 }
290 "TargetedPlayer" => {
291 if let Some(pid) = sa.target_chosen.target_player {
292 out_players.push(pid);
293 }
294 }
295 other => {
296 if let Some(pid) = resolve_defined_player(other, sa.activating_player, ctx.game)
298 {
299 out_players.push(pid);
300 } else {
301 let defined_cards = ability_utils::get_defined_cards(
302 ctx.game,
303 sa.source,
304 other,
305 Some(sa.activating_player),
306 );
307 out_cards.extend(defined_cards);
308 }
309 }
310 }
311 }
312 }
313
314 if let Some(defined) = sa.ir.remember_spell.as_deref() {
318 for token in defined.split(',').map(str::trim).filter(|s| !s.is_empty()) {
319 let spells = ability_utils::get_defined_spell_abilities(token, sa, ctx.game);
320 for spell in spells {
321 if let Some(src) = spell.source {
322 out_cards.push(src);
323 }
324 }
325 }
326 }
327
328 if let Some(defined) = sa.ir.remember_lki.as_deref() {
332 for token in defined.split(',').map(str::trim).filter(|s| !s.is_empty()) {
333 let cards = ability_utils::get_defined_cards(
334 ctx.game,
335 sa.source,
336 token,
337 Some(sa.activating_player),
338 );
339 for cid in cards {
340 out_lki_cards.push(crate::card::card_copy_service::get_lki_copy(
341 ctx.game.card(cid),
342 ));
343 }
344 }
345 }
346}
347
348struct ChosenSnapshot {
349 colors: Vec<String>,
350 cards: Vec<CardId>,
351 player: Option<PlayerId>,
352 player_controller: Option<PlayerId>,
353 player_revealed: bool,
354 type_: Option<String>,
355 type_controller: Option<PlayerId>,
356 type_revealed: bool,
357 type2: Option<String>,
358 named_cards: Vec<String>,
359 number: Option<i32>,
360}
361
362impl ChosenSnapshot {
363 fn capture(host: &Card) -> Self {
364 Self {
365 colors: host.chosen_colors.clone(),
366 cards: host.chosen_cards.clone(),
367 player: host.chosen_player,
368 player_controller: host.chosen_player_controller,
369 player_revealed: host.chosen_player_revealed,
370 type_: host.chosen_type.clone(),
371 type_controller: host.chosen_type_controller,
372 type_revealed: host.chosen_type_revealed,
373 type2: host.chosen_type2.clone(),
374 named_cards: host.named_cards.clone(),
375 number: host.chosen_number,
376 }
377 }
378}
379
380fn apply_chosen_state(
381 effect: &mut Card,
382 snap: &ChosenSnapshot,
383 chosen_number_override: Option<i32>,
384) {
385 if !snap.colors.is_empty() {
386 effect.clear_chosen_colors();
387 for c in &snap.colors {
388 effect.add_chosen_color(c.clone());
389 }
390 }
391 if !snap.cards.is_empty() {
392 effect.set_chosen_cards(snap.cards.clone());
393 }
394 if snap.player.is_some() {
395 effect.set_chosen_player(snap.player, snap.player_controller, snap.player_revealed);
396 }
397 if snap.type_.is_some() {
398 effect.set_chosen_type(snap.type_.clone(), snap.type_controller, snap.type_revealed);
399 }
400 if let Some(t2) = &snap.type2 {
401 effect.chosen_type2 = Some(t2.clone());
402 }
403 for name in &snap.named_cards {
404 effect.add_named_card(name);
405 }
406 if let Some(n) = chosen_number_override.or(snap.number) {
407 effect.set_chosen_number(Some(n));
408 }
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, strum_macros::EnumString, Default)]
414#[strum(ascii_case_insensitive)]
415pub enum EffectDuration {
416 #[default]
417 EndOfTurn,
418 Permanent,
419 UntilHostLeavesPlay,
420 UntilHostLeavesPlayOrEOT,
421}
422
423fn apply_duration_flags(effect: &mut Card, duration: Option<&AbilityDuration>, source_id: CardId) {
424 let d = match duration {
425 Some(AbilityDuration::UntilHostLeavesPlay) => EffectDuration::UntilHostLeavesPlay,
426 Some(AbilityDuration::UntilHostLeavesPlayOrEot) => EffectDuration::UntilHostLeavesPlayOrEOT,
427 Some(AbilityDuration::Unsupported(raw)) if raw.eq_ignore_ascii_case("Permanent") => {
428 EffectDuration::Permanent
429 }
430 _ => EffectDuration::EndOfTurn,
431 };
432 match d {
433 EffectDuration::Permanent => {}
434 EffectDuration::UntilHostLeavesPlay => {
435 effect.set_temp_effect_host(Some(source_id));
436 }
437 EffectDuration::UntilHostLeavesPlayOrEOT => {
438 effect.set_temp_effect_host(Some(source_id));
439 effect.set_temp_effect_until_eot(true);
440 }
441 EffectDuration::EndOfTurn => {
442 effect.set_temp_effect_until_eot(true);
443 }
444 }
445}
446
447fn apply_forget_on_moved_flags(effect: &mut Card, sa: &SpellAbility) {
448 if let Some(zone) = sa.ir.forget_on_moved_zone {
449 effect.set_forget_on_moved_origin(Some(zone));
450 effect.set_exile_when_no_remembered(true);
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use crate::ability::effects::EffectContext;
459 use crate::ability::spell_ability_effect::SpellAbilityEffect;
460 use crate::agent::{PassAgent, PlayerAgent};
461 use crate::card::Card;
462 use crate::game::GameState;
463 use crate::ids::{CardId, PlayerId};
464 use crate::mana::ManaPool;
465 use crate::spellability::SpellAbility;
466 use crate::trigger::handler::TriggerHandler;
467
468 #[test]
469 fn effect_adds_command_static_for_cast_with_flash() {
470 let mut game = GameState::new(&["P0", "P1"], 20);
471 let p0 = PlayerId(0);
472
473 let mut host = Card::new(
474 CardId(0),
475 "Winding Canyons".to_string(),
476 p0,
477 CardTypeLine::parse("Land"),
478 ManaCost::parse("0"),
479 ColorSet::COLORLESS,
480 None,
481 None,
482 vec![],
483 vec![],
484 );
485 host.set_s_var(
486 "GiveFlash",
487 "Mode$ CastWithFlash | ValidCard$ Creature | ValidSA$ Spell | Caster$ You",
488 );
489 let host_id = game.create_card(host);
490 game.move_card(host_id, ZoneType::Battlefield, p0);
491
492 let mut sa = SpellAbility::new_simple(
493 Some(host_id),
494 p0,
495 "AB$ Effect | StaticAbilities$ GiveFlash | SpellDescription$ test",
496 );
497 sa.is_activated = true;
498
499 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![Box::new(PassAgent), Box::new(PassAgent)];
500 let mut trigger_handler = TriggerHandler::new();
501 let token_templates = std::collections::HashMap::new();
502 let templates_variants: std::collections::HashMap<(String, String), usize> =
503 std::collections::HashMap::new();
504 let token_fallback: std::collections::HashMap<String, String> =
505 std::collections::HashMap::new();
506 let edition_dates: std::collections::HashMap<String, String> =
507 std::collections::HashMap::new();
508 let mut mana_pools = vec![ManaPool::new(), ManaPool::new()];
509 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
510 let mut ctx = EffectContext {
511 game: &mut game,
512 combat: None,
513 agents: &mut agents,
514 trigger_handler: &mut trigger_handler,
515 token_templates: &token_templates,
516 token_art_variants: &templates_variants,
517 token_fallback: &token_fallback,
518 edition_dates: &edition_dates,
519 mana_pools: &mut mana_pools,
520 parent_target_card: None,
521 rng: &mut rng_adapter,
522 };
523 EffectEffect::resolve(&mut ctx, &sa);
524
525 let command_cards = ctx.game.cards_in_zone(ZoneType::Command, p0).to_vec();
526 assert_eq!(command_cards.len(), 1);
527 let effect = ctx.game.card(command_cards[0]);
528 assert!(effect.temp_effect_until_eot);
529 assert_eq!(effect.effect_source, Some(host_id));
530
531 let mut spell_abilities = Vec::new();
532 spell_abilities.push("SP$ Permanent | Cost$ 1 G".to_string());
533 let fake_creature = Card::new(
534 CardId(999),
535 "Bear".to_string(),
536 p0,
537 CardTypeLine::parse("Creature - Bear"),
538 ManaCost::parse("1 G"),
539 ColorSet::GREEN,
540 Some(2),
541 Some(2),
542 vec![],
543 vec![],
544 );
545 assert!(
546 crate::staticability::static_ability_cast_with_flash::any_with_flash(
547 &ctx.game.cards,
548 &fake_creature,
549 p0,
550 &spell_abilities
551 )
552 );
553 }
554}