1use std::collections::BTreeMap;
36
37use forge_foundation::{CardTypeLine, CoreType, Supertype, ZoneType};
38
39use crate::agent::PlayerAgent;
40use crate::game::GameState;
41use crate::ids::{CardId, PlayerId};
42use crate::replacement::replacement_effect::ReplacementType;
43use crate::staticability::{CardFilter, Layer, StaticAbility, StaticMode};
44
45struct PendingEffect {
49 layer: Layer,
51 target: CardId,
53 kind: EffectKind,
55}
56
57enum EffectKind {
58 SetController {
59 controller: PlayerId,
60 },
61 AddPT {
62 power: i32,
63 toughness: i32,
64 },
65 SetPT {
66 power: Option<i32>,
67 toughness: Option<i32>,
68 },
69 RemoveAllCardTraits {
70 timestamp: i64,
71 static_id: i64,
72 },
73 GrantKeyword(String),
74 GrantAbility {
76 text: String,
77 svars: BTreeMap<String, String>,
78 },
79 AddType(String),
81 GrantTrigger {
83 text: String,
84 svars: BTreeMap<String, String>,
85 },
86}
87
88pub fn classify_static_layers(sa: &StaticAbility) -> Vec<Layer> {
96 if !sa.check_mode(&StaticMode::Continuous) {
97 return Vec::new();
98 }
99
100 let ir = &sa.ir;
101 let mut layers = Vec::new();
102
103 push_layer(&mut layers, ir.gain_control_param, Layer::Control);
104 push_layer(&mut layers, ir.has_text_layer_key, Layer::Text);
105 push_layer(&mut layers, ir.has_type_layer_key, Layer::Type);
106 push_layer(&mut layers, ir.has_color_layer_key, Layer::Color);
107 push_layer(&mut layers, ir.has_ability_layer_key, Layer::Ability);
108
109 if ir.set_power || ir.set_toughness {
110 if ir.characteristic_defining {
111 push_unique_layer(&mut layers, Layer::Characteristic);
112 } else {
113 push_unique_layer(&mut layers, Layer::SetPT);
114 }
115 }
116
117 push_layer(
118 &mut layers,
119 ir.add_power || ir.add_toughness,
120 Layer::ModifyPT,
121 );
122 push_layer(&mut layers, ir.has_rules_layer_key, Layer::Rules);
123
124 if layers.is_empty() {
125 layers.push(Layer::Rules);
126 }
127
128 layers
129}
130
131fn push_layer(layers: &mut Vec<Layer>, condition: bool, layer: Layer) {
132 if condition {
133 push_unique_layer(layers, layer);
134 }
135}
136
137fn static_layer_trait_id(source_id: CardId, sa_idx: usize) -> i64 {
138 -(((source_id.index() as i64) + 1) * 10_000 + sa_idx as i64 + 1)
139}
140
141fn push_unique_layer(layers: &mut Vec<Layer>, layer: Layer) {
142 if !layers.contains(&layer) {
143 layers.push(layer);
144 }
145}
146
147fn type_line_has_token(type_line: &CardTypeLine, token: &str) -> bool {
148 if let Some(st) = Supertype::from_name(token) {
149 return type_line.supertypes.contains(&st);
150 }
151 if let Some(ct) = CoreType::from_name(token) {
152 return type_line.core_types.contains(&ct);
153 }
154 type_line
155 .subtypes
156 .iter()
157 .any(|subtype| subtype.eq_ignore_ascii_case(token))
158}
159
160pub fn apply_continuous_effects(game: &mut GameState) {
172 let _perf_timer = crate::perf::ScopeTimer::start(
173 crate::perf::Metric::ContinuousEffectsCalls,
174 crate::perf::Metric::ContinuousEffectsNs,
175 );
176 let _params_lookup_scope =
177 crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Continuous);
178 for card in game.cards.iter_mut() {
180 card.clear_static_layer_changed_card_traits();
181 if card.activated_abilities.len() > card.base_ability_count {
184 card.activated_abilities.truncate(card.base_ability_count);
185 }
186 for (ability_idx, ability) in card.activated_abilities.iter_mut().enumerate() {
187 ability.ability_index = ability_idx;
188 }
189 let intrinsic_trigger_count = card.base_trigger_count + card.pump_trigger_count;
190 if card.triggers.len() > intrinsic_trigger_count {
191 card.triggers.truncate(intrinsic_trigger_count);
192 }
193 card.static_power_modifier = 0;
194 card.static_toughness_modifier = 0;
195 if !card.face_down {
197 card.static_set_power = None;
198 card.static_set_toughness = None;
199 }
200 card.granted_keywords.clear();
201 card.granted_svars.clear();
202 if let Some(type_line) = card.static_type_line_base.take() {
204 card.set_type_line(type_line);
205 }
206 card.static_added_subtypes.clear();
207 card.cant_attack_static = false;
208 card.cant_block_static = false;
209 }
210 for player in game.players.iter_mut() {
211 player.max_land_plays_per_turn = 1;
212 player.unlimited_land_plays = false;
213 }
214
215 for card in game.cards.iter_mut() {
218 if card.zone == ZoneType::Battlefield
219 && card.has_keyword("Unleash")
220 && card.counter_count(&crate::card::CounterType::P1P1) > 0
221 {
222 card.cant_block_static = true;
223 }
224 }
225
226 for player_idx in 0..game.player_order.len() {
227 let pid = game.player_order[player_idx];
228 let player = game.player_mut(pid);
229 player.max_hand_size = 7;
230 player.unlimited_hand_size = false;
231 }
232 let player_ids: Vec<PlayerId> = game.player_order.clone();
233 for &pid in &player_ids {
234 let battlefield_cards: Vec<CardId> =
235 game.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
236 for source_id in battlefield_cards {
237 let static_ability_count = game.card(source_id).static_abilities.len();
238 for sa_idx in 0..static_ability_count {
239 let card = game.card(source_id);
240 let sa = &card.static_abilities[sa_idx];
241 if !sa.check_conditions(card, game) {
242 continue;
243 }
244 if !sa.check_mode(&StaticMode::Continuous) {
245 continue;
246 }
247 let affected = sa.ir.affected_text.as_deref().unwrap_or("");
248 if !affected.eq_ignore_ascii_case("You") {
249 continue;
250 }
251 let controller = card.controller;
252 let set_value = sa.ir.set_max_hand_size.clone();
253 let raise_value = sa.ir.raise_max_hand_size.clone();
254 if let Some(value) = set_value {
255 let player = game.player_mut(controller);
256 if value.eq_ignore_ascii_case("Unlimited") {
257 player.unlimited_hand_size = true;
258 } else if let Ok(n) = value.parse::<i32>() {
259 player.max_hand_size = n;
260 }
261 }
262 if let Some(value) = raise_value {
263 if let Ok(n) = value.parse::<i32>() {
264 let player = game.player_mut(controller);
265 player.max_hand_size = player.max_hand_size.saturating_add(n);
266 }
267 }
268 }
269 }
270 }
271
272 let mut pending: Vec<PendingEffect> = Vec::new();
274 let mut cant_attack_targets: Vec<CardId> = Vec::new();
275 let mut cant_block_targets: Vec<CardId> = Vec::new();
276 let mut granted_player_rules: Vec<(CardId, StaticAbility)> = Vec::new();
277
278 let source_ids: Vec<CardId> = game.cards.iter().map(|card| card.id).collect();
279 for source_id in source_ids {
280 let static_ability_count = game.card(source_id).static_abilities.len();
281
282 for sa_idx in 0..static_ability_count {
283 let source_card = game.card(source_id).clone();
284 let sa = game.card(source_id).static_abilities[sa_idx].clone();
285
286 if !sa.check_conditions(&source_card, game) {
289 continue;
290 }
291
292 if sa.check_mode(&StaticMode::Continuous) {
293 apply_player_rules_effects(game, source_id, &sa);
294 }
295
296 let is_cda = sa.ir.characteristic_defining;
299
300 let affected_str = sa
302 .ir
303 .affected_text
304 .as_deref()
305 .or(sa.ir.valid_cards_text.as_deref())
306 .or(sa.ir.valid_card_text.as_deref())
307 .unwrap_or("Creature.YouControl");
308
309 let mut apply_to_target = |target: CardId| {
310 if sa.check_mode(&StaticMode::Continuous) {
311 if let Some(gain_control) = sa.ir.gain_control_text.as_deref() {
312 let new_controller = match gain_control {
313 "You" | "YouCtrl" => Some(source_card.controller),
314 "Opponent" => Some(game.opponent_of(source_card.controller)),
315 _ => None,
316 };
317 if let Some(controller) = new_controller {
318 pending.push(PendingEffect {
319 layer: Layer::Control,
320 target,
321 kind: EffectKind::SetController { controller },
322 });
323 }
324 }
325
326 let add_power = sa.ir.add_power_text.as_deref();
327 let add_toughness = sa.ir.add_toughness_text.as_deref();
328 if add_power.is_some() || add_toughness.is_some() {
329 let p = resolve_add_pt_value(game, source_id, add_power);
330 let t = resolve_add_pt_value(game, source_id, add_toughness);
331 pending.push(PendingEffect {
332 layer: Layer::ModifyPT,
333 target,
334 kind: EffectKind::AddPT {
335 power: p,
336 toughness: t,
337 },
338 });
339 }
340
341 let add_type = sa.ir.add_type_text.as_deref();
342 let source = game.card(source_id);
343 for added_type in resolve_added_types(source, add_type) {
344 pending.push(PendingEffect {
345 layer: Layer::Type,
346 target,
347 kind: EffectKind::AddType(added_type),
348 });
349 }
350
351 let set_power = sa.ir.set_power_text.as_deref();
352 let set_toughness = sa.ir.set_toughness_text.as_deref();
353 if set_power.is_some() || set_toughness.is_some() {
354 let sp = resolve_set_pt_value(game, source_id, set_power);
355 let st = resolve_set_pt_value(game, source_id, set_toughness);
356 let layer = if is_cda {
359 Layer::Characteristic
360 } else {
361 Layer::SetPT
362 };
363 pending.push(PendingEffect {
364 layer,
365 target,
366 kind: EffectKind::SetPT {
367 power: sp,
368 toughness: st,
369 },
370 });
371 }
372
373 if let Some(kws) = sa.ir.add_keyword_text.as_deref() {
374 for kw in kws.split('&').map(str::trim).filter(|s| !s.is_empty()) {
376 pending.push(PendingEffect {
377 layer: Layer::Ability,
378 target,
379 kind: EffectKind::GrantKeyword(kw.to_string()),
380 });
381 }
382 }
383
384 if sa.ir.remove_all_abilities {
385 pending.push(PendingEffect {
386 layer: Layer::Ability,
387 target,
388 kind: EffectKind::RemoveAllCardTraits {
389 timestamp: source_card.zone_timestamp as i64,
390 static_id: static_layer_trait_id(source_id, sa_idx),
391 },
392 });
393 }
394
395 if let Some(svar_name) = sa.ir.add_ability_text.as_deref() {
400 if let Some(ab_text) = source_card.svars.get(svar_name).cloned() {
401 pending.push(PendingEffect {
402 layer: Layer::Ability,
403 target,
404 kind: EffectKind::GrantAbility {
405 text: ab_text,
406 svars: source_card.svars.clone(),
407 },
408 });
409 }
410 }
411
412 if let Some(add_trigger) = sa.ir.add_trigger_text.as_deref() {
413 for svar_name in add_trigger
414 .split(" & ")
415 .map(str::trim)
416 .filter(|s| !s.is_empty())
417 {
418 if let Some(trig_text) = source_card.svars.get(svar_name).cloned() {
419 pending.push(PendingEffect {
420 layer: Layer::Ability,
421 target,
422 kind: EffectKind::GrantTrigger {
423 text: trig_text,
424 svars: source_card.svars.clone(),
425 },
426 });
427 }
428 }
429 }
430
431 if let Some(add_static) = sa.ir.add_static_ability_text.as_deref() {
432 for svar_name in add_static
433 .split(" & ")
434 .map(str::trim)
435 .filter(|s| !s.is_empty())
436 {
437 if let Some(static_text) = source_card.svars.get(svar_name).cloned() {
438 if let Some(granted) =
439 crate::staticability::parse_static_ability(&static_text)
440 {
441 granted_player_rules.push((target, granted));
442 }
443 }
444 }
445 }
446
447 for subtype in resolve_added_basic_land_types(&source_card, add_type) {
448 if let Some(ab_text) = basic_land_mana_ability_text(&subtype) {
449 pending.push(PendingEffect {
450 layer: Layer::Ability,
451 target,
452 kind: EffectKind::GrantAbility {
453 text: ab_text.to_string(),
454 svars: BTreeMap::new(),
455 },
456 });
457 }
458 }
459 }
460
461 if sa.check_mode(&StaticMode::CantAttack) {
462 cant_attack_targets.push(target);
463 }
464 if sa.check_mode(&StaticMode::CantBlock) {
465 cant_block_targets.push(target);
466 }
467 };
468
469 if is_cda {
470 if source_card.zone == ZoneType::Battlefield {
472 apply_to_target(source_id);
473 }
474 } else if affected_str.eq_ignore_ascii_case("Card.Self")
475 || affected_str.starts_with("Card.Self+")
476 {
477 if source_card.zone == ZoneType::Battlefield
484 && crate::card::valid_filter::matches_valid_card(
485 affected_str,
486 &source_card,
487 &source_card,
488 )
489 {
490 apply_to_target(source_id);
491 }
492 } else if affected_str.eq_ignore_ascii_case("Card.EnchantedBy")
493 || affected_str.contains(".EquippedBy")
494 || affected_str.contains(".EnchantedBy")
495 {
496 if let Some(cid) = source_card.attached_to {
502 if game.card(cid).zone == ZoneType::Battlefield {
503 apply_to_target(cid);
504 }
505 }
506 } else {
507 let filter = CardFilter::parse(affected_str);
508 let affected_zones = if sa.ir.affected_zones.is_empty() {
511 None
512 } else {
513 Some(sa.ir.affected_zones.as_slice())
514 };
515 for card in &game.cards {
516 let zone_matches = match &affected_zones {
517 Some(zones) => zones.contains(&card.zone),
518 None => card.zone == ZoneType::Battlefield,
519 };
520 if zone_matches && filter.matches_with_game(card, &source_card, game) {
521 apply_to_target(card.id);
522 }
523 }
524 }
525 }
526 }
527
528 for (source_id, granted) in granted_player_rules {
529 apply_player_rules_effects(game, source_id, &granted);
530 }
531
532 for target in cant_attack_targets {
533 game.cards[target.index()].cant_attack_static = true;
534 }
535 for target in cant_block_targets {
536 game.cards[target.index()].cant_block_static = true;
537 }
538
539 pending.sort_by_key(|e| e.layer);
544
545 for effect in pending {
546 match effect.kind {
547 EffectKind::SetController { controller } => {
548 game.change_controller(effect.target, controller);
549 }
550 EffectKind::AddPT { power, toughness } => {
551 let card = &mut game.cards[effect.target.index()];
552 card.static_power_modifier += power;
553 card.static_toughness_modifier += toughness;
554 }
555 EffectKind::SetPT { power, toughness } => {
556 let card = &mut game.cards[effect.target.index()];
557 if let Some(p) = power {
561 card.static_set_power = Some(p);
562 }
563 if let Some(t) = toughness {
564 card.static_set_toughness = Some(t);
565 }
566 }
567 EffectKind::RemoveAllCardTraits {
568 timestamp,
569 static_id,
570 } => {
571 game.cards[effect.target.index()].add_changed_card_traits(
572 crate::card::card_trait_changes::CardTraitChanges::remove_all_layer(
573 Vec::new(),
574 Vec::new(),
575 Vec::new(),
576 Vec::new(),
577 ),
578 timestamp,
579 static_id,
580 );
581 }
582 EffectKind::GrantKeyword(kw) => {
583 let card = &mut game.cards[effect.target.index()];
584 card.granted_keywords.add(&kw);
585 if let Some(cost_str) = crate::keyword::extract_keyword_cost_str(&kw, "Ward") {
586 let next_id = card
587 .triggers
588 .iter()
589 .map(|t| t.id)
590 .max()
591 .unwrap_or(0)
592 .saturating_add(1);
593 let mut next_id_mut = next_id;
594 let execute = format!("TrigWardGranted{next_id}");
595 let raw = format!(
596 "Mode$ BecomesTarget | ValidSource$ SpellAbility.OppCtrl | ValidTarget$ Card.Self | Secondary$ True | Execute$ {execute} | TriggerZones$ Battlefield | TriggerDescription$ Ward"
597 );
598 if let Some(mut trig) = crate::trigger::parse_trigger(&raw, &mut next_id_mut) {
599 trig.execute = execute.clone();
600 card.add_trigger(trig);
601 }
602 card.granted_svars.insert(
603 execute,
604 format!(
605 "DB$ Counter | Defined$ TriggeredSourceSA | UnlessCost$ {cost_str}"
606 ),
607 );
608 }
609 }
610 EffectKind::AddType(t) => {
611 let card = &mut game.cards[effect.target.index()];
612 if !type_line_has_token(&card.type_line, &t) {
613 if card.static_type_line_base.is_none() {
614 card.static_type_line_base = Some(card.type_line.clone());
615 }
616 card.add_type(&t);
617 card.static_added_subtypes.push(t);
618 }
619 }
620 EffectKind::GrantAbility { text, svars } => {
621 game.cards[effect.target.index()]
624 .granted_svars
625 .extend(svars);
626 let target_idx = effect.target.index();
627 let next_idx = game.cards[target_idx].activated_abilities.len();
628 if let Some(ab) =
629 crate::ability::activated::parse_activated_ability(&text, next_idx)
630 {
631 game.cards[target_idx].activated_abilities.push(ab);
632 }
633 }
634 EffectKind::GrantTrigger { text, svars } => {
635 game.cards[effect.target.index()]
636 .granted_svars
637 .extend(svars);
638 let next_id = game.cards[effect.target.index()]
639 .triggers
640 .iter()
641 .map(|t| t.id)
642 .max()
643 .unwrap_or(0)
644 .saturating_add(1);
645 let mut next_id_mut = next_id;
646 if let Some(trig) = crate::trigger::parse_trigger(&text, &mut next_id_mut) {
647 game.cards[effect.target.index()].add_trigger(trig);
648 }
649 }
650 }
651 }
652
653 for card in game.cards.iter_mut() {
656 if card.zone == ZoneType::Battlefield {
657 card.generate_basic_land_mana_abilities();
658 }
659 }
660}
661
662fn apply_player_rules_effects(game: &mut GameState, source_id: CardId, sa: &StaticAbility) {
663 let Some(adjust_land_plays) = sa.ir.adjust_land_plays_text.as_deref() else {
664 return;
665 };
666 let affected_players = affected_players_for_static(game, source_id, sa);
667 if affected_players.is_empty() {
668 return;
669 }
670 if adjust_land_plays.eq_ignore_ascii_case("Unlimited") {
671 for player in affected_players {
672 game.player_mut(player).unlimited_land_plays = true;
673 }
674 return;
675 }
676 let amount = resolve_rules_amount(game, source_id, adjust_land_plays);
677 for player in affected_players {
678 game.player_mut(player).max_land_plays_per_turn += amount;
679 }
680}
681
682fn affected_players_for_static(
683 game: &GameState,
684 source_id: CardId,
685 sa: &StaticAbility,
686) -> Vec<PlayerId> {
687 let Some(affected) = sa.ir.affected_text.as_deref() else {
688 return Vec::new();
689 };
690 let source = game.card(source_id);
691 game.player_order
692 .iter()
693 .copied()
694 .filter(|&player| {
695 !sa.ignore_effect_players.contains(&player)
696 && crate::card::valid_filter::matches_valid(
697 affected,
698 None,
699 Some(player),
700 source,
701 source.controller,
702 )
703 })
704 .collect()
705}
706
707fn resolve_rules_amount(game: &GameState, source_id: CardId, value: &str) -> i32 {
708 if let Ok(n) = value.trim().parse::<i32>() {
709 return n;
710 }
711 let source = game.card(source_id);
712 if let Some(svar_expr) = source.svars.get(value.trim()) {
713 if svar_expr.starts_with("Count$") {
714 return crate::ability::effects::resolve_count_svar(
715 svar_expr,
716 game,
717 source_id,
718 source.controller,
719 );
720 }
721 return crate::ability::effects::evaluate_svar(
722 svar_expr,
723 &crate::spellability::SpellAbility::new_empty(Some(source_id), source.controller),
724 );
725 }
726 0
727}
728
729pub fn apply_etb_tapped(game: &mut GameState, entering_card: CardId) {
739 apply_etb_tapped_with_agents(game, entering_card, None);
740}
741
742fn applicable_etb_tapped_replacement_sources(
743 game: &GameState,
744 entering_card: CardId,
745) -> Vec<(CardId, String)> {
746 let mut repl_sources: Vec<(CardId, String, String)> = Vec::new();
747 for c in &game.cards {
748 if c.zone != ZoneType::Battlefield {
749 continue;
750 }
751 for re in &c.replacement_effects {
752 if re.event == ReplacementType::Moved
753 && re.replace_with() == Some("ETBTapped")
754 && re.ir.destination_zone == Some(ZoneType::Battlefield)
755 && re.active_in_zone(ZoneType::Battlefield)
756 {
757 let filter = re
758 .ir
759 .valid_card_text
760 .as_deref()
761 .unwrap_or("Card.Self")
762 .to_string();
763 let desc = re.description(c, game);
764 repl_sources.push((c.id, filter, desc));
765 }
766 }
767 }
768
769 repl_sources
770 .into_iter()
771 .filter_map(|(source_id, filter_str, desc)| {
772 let tapped = if filter_str == "Card.Self" || filter_str.is_empty() {
773 source_id == entering_card
774 } else {
775 let source = &game.cards[source_id.index()];
776 let filter = CardFilter::parse(&filter_str);
777 filter.matches_with_game(&game.cards[entering_card.index()], source, game)
778 };
779 tapped.then_some((source_id, desc))
780 })
781 .collect()
782}
783
784pub fn prompt_etb_tapped_replacement_with_agents(
785 game: &mut GameState,
786 entering_card: CardId,
787 agents: &mut [Box<dyn PlayerAgent>],
788) {
789 let applicable = applicable_etb_tapped_replacement_sources(game, entering_card);
790 if applicable.is_empty() {
791 return;
792 }
793
794 let affected_player = game.cards[entering_card.index()].controller;
795 let descriptions: Vec<String> = applicable
796 .iter()
797 .map(|(source_id, desc)| format!("{}: {}", game.card(*source_id).card_name, desc))
798 .collect();
799 let _chosen = agents[affected_player.index()]
800 .choose_single_replacement_effect(affected_player, &descriptions)
801 .min(applicable.len().saturating_sub(1));
802}
803
804pub fn apply_etb_tapped_with_agents(
805 game: &mut GameState,
806 entering_card: CardId,
807 agents: Option<&mut [Box<dyn PlayerAgent>]>,
808) {
809 let etb_sources: Vec<(CardId, String)> = game
812 .cards
813 .iter()
814 .filter(|c| c.zone == ZoneType::Battlefield)
815 .flat_map(|c| {
816 c.static_abilities.iter().filter_map(move |sa| {
817 if sa.check_mode(&StaticMode::ETBTapped) {
818 let filter_str = sa
819 .ir
820 .valid_cards_text
821 .clone()
822 .or_else(|| sa.ir.affected_text.clone())
823 .unwrap_or_else(|| "Card.Self".to_string());
825 Some((c.id, filter_str))
826 } else {
827 None
828 }
829 })
830 })
831 .collect();
832
833 for (source_id, filter_str) in etb_sources {
834 let tapped = if filter_str == "Card.Self" || filter_str.is_empty() {
836 source_id == entering_card
837 } else {
838 let source = &game.cards[source_id.index()];
839 let filter = CardFilter::parse(&filter_str);
840 filter.matches_with_game(&game.cards[entering_card.index()], source, game)
841 };
842
843 if tapped {
844 game.cards[entering_card.index()].tapped = true;
845 return; }
847 }
848
849 let applicable = applicable_etb_tapped_replacement_sources(game, entering_card);
854 if applicable.is_empty() {
855 return;
856 }
857
858 if let Some(agents) = agents {
859 prompt_etb_tapped_replacement_with_agents(game, entering_card, agents);
860 }
861
862 game.cards[entering_card.index()].tapped = true;
863}
864
865pub fn get_etb_unless_life_cost(card: &crate::card::Card) -> Option<i32> {
873 for re in &card.replacement_effects {
874 if re.event != ReplacementType::Moved {
875 continue;
876 }
877 if re.ir.destination_zone != Some(ZoneType::Battlefield) {
878 continue;
879 }
880 if let Some(svar_name) = re.replace_with() {
881 if svar_name == "ETBTapped" {
882 continue;
883 }
884 if let Some(svar_val) = card.svars.get(svar_name) {
885 if svar_val.contains("DB$ Tap") && svar_val.contains("ETB$ True") {
886 if let Some(pos) = svar_val.find("PayLife<") {
888 let after = &svar_val[pos + 8..]; if let Some(end) = after.find('>') {
890 if let Ok(n) = after[..end].parse::<i32>() {
891 return Some(n);
892 }
893 }
894 }
895 }
896 }
897 }
898 }
899 None
900}
901
902pub fn get_etb_unless_reveal_cost(card: &crate::card::Card) -> Option<(i32, String)> {
909 for re in &card.replacement_effects {
910 if re.event != ReplacementType::Moved {
911 continue;
912 }
913 if re.ir.destination_zone != Some(ZoneType::Battlefield) {
914 continue;
915 }
916 if let Some(svar_name) = re.replace_with() {
917 if svar_name == "ETBTapped" {
918 continue;
919 }
920 if let Some(svar_val) = card.svars.get(svar_name) {
921 if svar_val.contains("DB$ Tap") && svar_val.contains("ETB$ True") {
922 if let Some(pos) = svar_val.find("Reveal<") {
924 let after = &svar_val[pos + 7..]; if let Some(end) = after.find('>') {
926 let inner = &after[..end]; let mut parts = inner.splitn(2, '/');
928 let n = parts
929 .next()
930 .and_then(|s| s.trim().parse::<i32>().ok())
931 .unwrap_or(1);
932 let filter = parts.next().unwrap_or("").trim().to_string();
933 return Some((n, filter));
934 }
935 }
936 }
937 }
938 }
939 }
940 None
941}
942
943fn resolve_add_pt_value(game: &GameState, source_id: CardId, val_str: Option<&str>) -> i32 {
946 let val_str = match val_str {
947 Some(val_str) => val_str,
948 None => return 0,
949 };
950
951 if let Ok(n) = val_str.trim().parse::<i32>() {
953 return n;
954 }
955
956 let source = game.card(source_id);
958 if let Some(svar_expr) = source.svars.get(val_str.trim()) {
959 if svar_expr.starts_with("Count$") {
960 return crate::ability::effects::resolve_count_svar(
961 svar_expr,
962 game,
963 source_id,
964 source.controller,
965 );
966 }
967 return crate::ability::effects::evaluate_svar(
968 svar_expr,
969 &crate::spellability::SpellAbility::new_empty(Some(source_id), source.controller),
970 );
971 }
972
973 0
974}
975
976fn resolve_set_pt_value(game: &GameState, source_id: CardId, val_str: Option<&str>) -> Option<i32> {
980 let val_str = val_str?;
981 if let Ok(n) = val_str.trim().parse::<i32>() {
983 return Some(n);
984 }
985
986 let source = game.card(source_id);
988 if let Some(svar_expr) = source.svars.get(val_str.trim()) {
989 if svar_expr.starts_with("Count$") {
990 return Some(crate::ability::effects::resolve_count_svar(
991 svar_expr,
992 game,
993 source_id,
994 source.controller,
995 ));
996 }
997 return Some(crate::ability::effects::evaluate_svar(
999 svar_expr,
1000 &crate::spellability::SpellAbility::new_empty(Some(source_id), source.controller),
1001 ));
1002 }
1003
1004 None
1005}
1006
1007fn basic_land_mana_ability_text(subtype: &str) -> Option<&'static str> {
1008 match subtype {
1009 "Plains" => Some("AB$ Mana | Cost$ T | Produced$ W | SpellDescription$ Add {W}."),
1010 "Island" => Some("AB$ Mana | Cost$ T | Produced$ U | SpellDescription$ Add {U}."),
1011 "Swamp" => Some("AB$ Mana | Cost$ T | Produced$ B | SpellDescription$ Add {B}."),
1012 "Mountain" => Some("AB$ Mana | Cost$ T | Produced$ R | SpellDescription$ Add {R}."),
1013 "Forest" => Some("AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}."),
1014 _ => None,
1015 }
1016}
1017
1018fn resolve_added_basic_land_types(
1019 source: &crate::card::Card,
1020 add_type: Option<&str>,
1021) -> Vec<String> {
1022 resolve_added_types(source, add_type)
1023 .into_iter()
1024 .filter(|added| basic_land_mana_ability_text(added).is_some())
1025 .collect()
1026}
1027
1028fn resolve_added_types(source: &crate::card::Card, add_type: Option<&str>) -> Vec<String> {
1029 let Some(add_type) = add_type else {
1030 return Vec::new();
1031 };
1032 let mut resolved = Vec::new();
1033 for raw in add_type.split('&').map(str::trim).filter(|s| !s.is_empty()) {
1034 match raw {
1035 "ChosenType" => {
1036 if let Some(chosen) = source.chosen_type.as_ref() {
1037 resolved.push(chosen.clone());
1038 }
1039 }
1040 "ChosenType2" => {
1041 if let Some(chosen) = source.chosen_type2.as_ref() {
1042 resolved.push(chosen.clone());
1043 }
1044 }
1045 "AllBasicLandType" => {
1046 resolved.extend(
1047 ["Plains", "Island", "Swamp", "Mountain", "Forest"]
1048 .into_iter()
1049 .map(str::to_string),
1050 );
1051 }
1052 other => resolved.push(other.to_string()),
1053 }
1054 }
1055 resolved
1056}
1057
1058#[cfg(test)]
1061mod tests {
1062 use super::*;
1063 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
1064
1065 use crate::card::Card;
1066 use crate::ids::{CardId, PlayerId};
1067
1068 fn new_game() -> GameState {
1070 GameState::new(&["Alice", "Bob"], 20)
1071 }
1072
1073 fn add_creature(
1074 game: &mut GameState,
1075 owner: PlayerId,
1076 power: i32,
1077 toughness: i32,
1078 keywords: Vec<String>,
1079 abilities: Vec<String>,
1080 ) -> CardId {
1081 let card = Card::new(
1082 CardId(0), "Creature".to_string(),
1084 owner,
1085 CardTypeLine::parse("Creature"),
1086 ManaCost::parse("1 G"),
1087 ColorSet::GREEN,
1088 Some(power),
1089 Some(toughness),
1090 keywords,
1091 abilities,
1092 );
1093 let id = game.create_card(card);
1094 game.move_card(id, ZoneType::Battlefield, owner);
1095 id
1096 }
1097
1098 fn add_enchantment(game: &mut GameState, owner: PlayerId, abilities: Vec<String>) -> CardId {
1099 let card = Card::new(
1100 CardId(0),
1101 "Enchantment".to_string(),
1102 owner,
1103 CardTypeLine::parse("Enchantment"),
1104 ManaCost::parse("2 W"),
1105 ColorSet::WHITE,
1106 None,
1107 None,
1108 vec![],
1109 abilities,
1110 );
1111 let id = game.create_card(card);
1112 game.move_card(id, ZoneType::Battlefield, owner);
1113 id
1114 }
1115
1116 fn add_land(
1117 game: &mut GameState,
1118 owner: PlayerId,
1119 name: &str,
1120 type_line: &str,
1121 abilities: Vec<String>,
1122 ) -> CardId {
1123 let card = Card::new(
1124 CardId(0),
1125 name.to_string(),
1126 owner,
1127 CardTypeLine::parse(type_line),
1128 ManaCost::no_cost(),
1129 ColorSet::COLORLESS,
1130 None,
1131 None,
1132 vec![],
1133 abilities,
1134 );
1135 let id = game.create_card(card);
1136 game.move_card(id, ZoneType::Battlefield, owner);
1137 id
1138 }
1139
1140 fn add_effect(game: &mut GameState, owner: PlayerId, abilities: Vec<String>) -> CardId {
1141 let card = Card::new(
1142 CardId(0),
1143 "Effect".to_string(),
1144 owner,
1145 CardTypeLine::parse("Effect"),
1146 ManaCost::parse("0"),
1147 ColorSet::COLORLESS,
1148 None,
1149 None,
1150 vec![],
1151 abilities,
1152 );
1153 let id = game.create_card(card);
1154 game.move_card(id, ZoneType::Command, owner);
1155 id
1156 }
1157
1158 #[test]
1161 fn anthem_boosts_your_creatures() {
1162 let mut game = new_game();
1163 let alice = PlayerId(0);
1164 let bob = PlayerId(1);
1165
1166 let a1 = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1168 let a2 = add_creature(&mut game, alice, 1, 1, vec![], vec![]);
1169 let b1 = add_creature(&mut game, bob, 2, 2, vec![], vec![]);
1170
1171 let _anthem = add_enchantment(
1173 &mut game,
1174 alice,
1175 vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1 | Description$ Creatures you control get +1/+1.".to_string()],
1176 );
1177
1178 apply_continuous_effects(&mut game);
1179
1180 assert_eq!(game.card(a1).power(), 3, "Alice's 2/2 should be 3/3");
1182 assert_eq!(game.card(a1).toughness(), 3);
1183 assert_eq!(game.card(a2).power(), 2, "Alice's 1/1 should be 2/2");
1184 assert_eq!(game.card(a2).toughness(), 2);
1185
1186 assert_eq!(
1188 game.card(b1).power(),
1189 2,
1190 "Bob's creature should be unchanged"
1191 );
1192 assert_eq!(game.card(b1).toughness(), 2);
1193 }
1194
1195 #[test]
1196 fn command_effect_adjusts_land_plays_for_affected_player() {
1197 let mut game = new_game();
1198 let alice = PlayerId(0);
1199 let bob = PlayerId(1);
1200
1201 let effect = add_effect(
1202 &mut game,
1203 alice,
1204 vec![
1205 "S$ Mode$ Continuous | EffectZone$ Command | Affected$ You | AdjustLandPlays$ 1"
1206 .to_string(),
1207 ],
1208 );
1209
1210 apply_continuous_effects(&mut game);
1211
1212 assert_eq!(game.player(alice).max_land_plays_per_turn, 2);
1213 assert_eq!(game.player(bob).max_land_plays_per_turn, 1);
1214
1215 game.move_card(effect, ZoneType::Exile, alice);
1216 apply_continuous_effects(&mut game);
1217
1218 assert_eq!(game.player(alice).max_land_plays_per_turn, 1);
1219 }
1220
1221 #[test]
1222 fn anthem_resets_when_removed() {
1223 let mut game = new_game();
1224 let alice = PlayerId(0);
1225
1226 let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1227 let anthem = add_enchantment(
1228 &mut game,
1229 alice,
1230 vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1".to_string()],
1231 );
1232
1233 apply_continuous_effects(&mut game);
1234 assert_eq!(game.card(creature).power(), 3);
1235
1236 game.move_card(anthem, ZoneType::Graveyard, alice);
1238 apply_continuous_effects(&mut game);
1239
1240 assert_eq!(
1241 game.card(creature).power(),
1242 2,
1243 "Bonus should be gone after anthem leaves"
1244 );
1245 }
1246
1247 #[test]
1248 fn stacking_anthems() {
1249 let mut game = new_game();
1250 let alice = PlayerId(0);
1251
1252 let creature = add_creature(&mut game, alice, 1, 1, vec![], vec![]);
1253 add_enchantment(
1255 &mut game,
1256 alice,
1257 vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1".to_string()],
1258 );
1259 add_enchantment(
1260 &mut game,
1261 alice,
1262 vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1".to_string()],
1263 );
1264
1265 apply_continuous_effects(&mut game);
1266 assert_eq!(game.card(creature).power(), 3, "Two anthems should give +2");
1267 assert_eq!(game.card(creature).toughness(), 3);
1268 }
1269
1270 #[test]
1273 fn grant_flying_to_your_creatures() {
1274 let mut game = new_game();
1275 let alice = PlayerId(0);
1276 let bob = PlayerId(1);
1277
1278 let a1 = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1279 let b1 = add_creature(&mut game, bob, 2, 2, vec![], vec![]);
1280
1281 add_enchantment(
1282 &mut game,
1283 alice,
1284 vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddKeyword$ Flying | Description$ Creatures you control have flying.".to_string()],
1285 );
1286
1287 apply_continuous_effects(&mut game);
1288
1289 assert!(
1290 game.card(a1).has_flying(),
1291 "Alice's creature should have flying"
1292 );
1293 assert!(
1294 !game.card(b1).has_flying(),
1295 "Bob's creature should not have flying"
1296 );
1297 }
1298
1299 #[test]
1300 fn grant_multiple_keywords() {
1301 let mut game = new_game();
1302 let alice = PlayerId(0);
1303
1304 let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1305 add_enchantment(
1306 &mut game,
1307 alice,
1308 vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddKeyword$ Flying & First Strike".to_string()],
1309 );
1310
1311 apply_continuous_effects(&mut game);
1312
1313 assert!(game.card(creature).has_flying());
1314 assert!(game.card(creature).has_first_strike());
1315 }
1316
1317 #[test]
1320 fn set_pt_overrides_base() {
1321 let mut game = new_game();
1322 let alice = PlayerId(0);
1323
1324 let creature = add_creature(&mut game, alice, 5, 5, vec![], vec![]);
1325 add_enchantment(
1327 &mut game,
1328 alice,
1329 vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | SetPower$ 0 | SetToughness$ 1".to_string()],
1330 );
1331
1332 apply_continuous_effects(&mut game);
1333 assert_eq!(game.card(creature).power(), 0);
1334 assert_eq!(game.card(creature).toughness(), 1);
1335 }
1336
1337 #[test]
1338 fn modify_pt_adds_on_top_of_set_pt() {
1339 let mut game = new_game();
1341 let alice = PlayerId(0);
1342
1343 let creature = add_creature(&mut game, alice, 5, 5, vec![], vec![]);
1344 add_enchantment(
1346 &mut game,
1347 alice,
1348 vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | SetPower$ 0 | SetToughness$ 1".to_string()],
1349 );
1350 add_enchantment(
1352 &mut game,
1353 alice,
1354 vec!["S$ Mode$ Continuous | Affected$ Creature.YouControl | AddPower$ 1 | AddToughness$ 1".to_string()],
1355 );
1356
1357 apply_continuous_effects(&mut game);
1358 assert_eq!(game.card(creature).power(), 1);
1360 assert_eq!(game.card(creature).toughness(), 2);
1361 }
1362
1363 #[test]
1366 fn cant_attack_flag_set() {
1367 let mut game = new_game();
1368 let alice = PlayerId(0);
1369
1370 let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1371 add_enchantment(
1373 &mut game,
1374 alice,
1375 vec!["S$ Mode$ CantAttack | Affected$ Creature.YouControl | Description$ Creatures you control can't attack.".to_string()],
1376 );
1377
1378 apply_continuous_effects(&mut game);
1379 assert!(game.card(creature).cant_attack_static);
1380 }
1381
1382 #[test]
1383 fn cant_block_flag_set() {
1384 let mut game = new_game();
1385 let alice = PlayerId(0);
1386
1387 let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1388 add_enchantment(
1389 &mut game,
1390 alice,
1391 vec!["S$ Mode$ CantBlock | Affected$ Creature.YouControl".to_string()],
1392 );
1393
1394 apply_continuous_effects(&mut game);
1395 assert!(game.card(creature).cant_block_static);
1396 }
1397
1398 #[test]
1399 fn flags_reset_on_reapplication() {
1400 let mut game = new_game();
1401 let alice = PlayerId(0);
1402
1403 let creature = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1404 let restrictor = add_enchantment(
1405 &mut game,
1406 alice,
1407 vec!["S$ Mode$ CantAttack | Affected$ Creature.YouControl".to_string()],
1408 );
1409
1410 apply_continuous_effects(&mut game);
1411 assert!(game.card(creature).cant_attack_static);
1412
1413 game.move_card(restrictor, ZoneType::Graveyard, alice);
1414 apply_continuous_effects(&mut game);
1415 assert!(
1416 !game.card(creature).cant_attack_static,
1417 "Flag should clear after enchantment leaves"
1418 );
1419 }
1420
1421 #[test]
1422 fn lands_gain_swamp_mana_ability_from_urborg_style_effect() {
1423 let mut game = new_game();
1424 let alice = PlayerId(0);
1425
1426 let urborg = add_land(
1427 &mut game,
1428 alice,
1429 "Urborg, Tomb of Yawgmoth",
1430 "Legendary Land",
1431 vec!["S$ Mode$ Continuous | Affected$ Land | AddType$ Swamp | Description$ Each land is a Swamp in addition to its other land types.".to_string()],
1432 );
1433 let black_gate = add_land(
1434 &mut game,
1435 alice,
1436 "The Black Gate",
1437 "Legendary Land Gate",
1438 vec![],
1439 );
1440
1441 apply_continuous_effects(&mut game);
1442
1443 for land_id in [urborg, black_gate] {
1444 let land = game.card(land_id);
1445 assert!(
1446 land.type_line.has_subtype("Swamp"),
1447 "{} should gain the Swamp subtype",
1448 land.card_name
1449 );
1450 assert!(
1451 land.activated_abilities.iter().any(|ab| {
1452 ab.is_mana_ability
1453 && ab
1454 .produced_ir
1455 .as_ref()
1456 .is_some_and(|ir| ir.as_script_text() == "B")
1457 }),
1458 "{} should gain an intrinsic black mana ability from Swamp",
1459 land.card_name
1460 );
1461 }
1462 }
1463
1464 #[test]
1467 fn self_etb_tapped() {
1468 let mut game = new_game();
1469 let alice = PlayerId(0);
1470
1471 let card = Card::new(
1473 CardId(0),
1474 "TappedLand".to_string(),
1475 alice,
1476 CardTypeLine::parse("Land"),
1477 ManaCost::parse(""),
1478 ColorSet::from_mask(0),
1479 None,
1480 None,
1481 vec![],
1482 vec!["S$ Mode$ ETBTapped | Description$ Enters tapped.".to_string()],
1483 );
1484 let id = game.create_card(card);
1485 game.move_card(id, ZoneType::Battlefield, alice);
1486 apply_etb_tapped(&mut game, id);
1487
1488 assert!(
1489 game.card(id).tapped,
1490 "Card with ETBTapped should enter tapped"
1491 );
1492 }
1493
1494 #[test]
1495 fn no_etb_tapped_without_ability() {
1496 let mut game = new_game();
1497 let alice = PlayerId(0);
1498
1499 let id = add_creature(&mut game, alice, 2, 2, vec![], vec![]);
1500 assert!(
1502 !game.card(id).tapped,
1503 "Normal creature should not enter tapped"
1504 );
1505 }
1506
1507 #[test]
1508 fn etb_tapped_via_replacement_effect() {
1509 let mut game = new_game();
1510 let alice = PlayerId(0);
1511
1512 let card = Card::new(
1514 CardId(0),
1515 "PathOfAncestry".to_string(),
1516 alice,
1517 CardTypeLine::parse("Land"),
1518 ManaCost::parse(""),
1519 ColorSet::from_mask(0),
1520 None,
1521 None,
1522 vec![],
1523 vec!["R:Event$ Moved | Destination$ Battlefield | ValidCard$ Card.Self | ReplaceWith$ ETBTapped | Description$ ~ enters tapped.".to_string()],
1524 );
1525 let id = game.create_card(card);
1526 game.move_card(id, ZoneType::Battlefield, alice);
1527 apply_etb_tapped(&mut game, id);
1528
1529 assert!(
1530 game.card(id).tapped,
1531 "Card with ReplaceWith$ ETBTapped replacement should enter tapped"
1532 );
1533 }
1534}