1use forge_foundation::{ColorSet, ZoneType};
9
10use crate::ability::AbilityKey;
11use crate::card::filter_constants as fc;
12use crate::card::{valid_filter, Card, CounterType};
13use crate::game::GameState;
14use crate::ids::{CardId, PlayerId};
15use crate::parsing::{cached_compiled_selector, CompiledSelector};
16use crate::spellability::SpellAbility;
17
18fn push_unique_player(players: &mut Vec<PlayerId>, player: PlayerId) {
19 if !players.contains(&player) {
20 players.push(player);
21 }
22}
23
24fn targeted_spell_abilities(sa: &SpellAbility, game: &GameState) -> Vec<SpellAbility> {
25 let mut spells = Vec::new();
26 if let Some(stack_id) = sa.target_chosen.target_stack_entry {
27 if let Some(entry) = game.stack.find_by_id(stack_id) {
28 unique_push_spell(&mut spells, entry.spell_ability.clone());
29 }
30 }
31 spells
32}
33
34fn targeted_controller_players(sa: &SpellAbility, game: &GameState) -> Vec<PlayerId> {
35 let mut players = Vec::new();
36 if let Some(cid) = sa.target_chosen.target_card {
37 push_unique_player(&mut players, game.card(cid).controller);
38 }
39 for spell in targeted_spell_abilities(sa, game) {
40 push_unique_player(&mut players, spell.activating_player);
41 }
42 players
43}
44
45fn targeted_owner_players(sa: &SpellAbility, game: &GameState) -> Vec<PlayerId> {
46 let mut players = Vec::new();
47 if let Some(cid) = sa.target_chosen.target_card {
48 push_unique_player(&mut players, game.card(cid).owner);
49 }
50 for spell in targeted_spell_abilities(sa, game) {
51 if let Some(source) = spell.source {
52 push_unique_player(&mut players, game.card(source).owner);
53 }
54 }
55 players
56}
57
58fn add_players_from_remembered(
59 players: &mut Vec<PlayerId>,
60 game: &GameState,
61 card_id: CardId,
62 def: &str,
63 skip_remembered: bool,
64) {
65 let remembered_players = game.card(card_id).remembered_players.clone();
66 for player in remembered_players {
67 if def.ends_with("Opponents") {
68 push_unique_player(players, game.opponent_of(player));
69 } else {
70 push_unique_player(players, player);
71 }
72 }
73
74 let remembered_cards = game.card(card_id).remembered_cards.clone();
75 for remembered_card in remembered_cards {
76 if def.ends_with("Controller") {
77 push_unique_player(players, game.card(remembered_card).controller);
78 } else if def.ends_with("Owner") {
79 push_unique_player(players, game.card(remembered_card).owner);
80 } else if def.ends_with("Remembered") && !skip_remembered {
81 add_players_from_remembered(players, game, remembered_card, def, true);
82 }
83 }
84}
85
86fn remembered_players_for_def(def: &str, sa: &SpellAbility, game: &GameState) -> Vec<PlayerId> {
87 let mut players = Vec::new();
88 if let Some(source) = sa.source {
89 add_players_from_remembered(&mut players, game, source, def, false);
90 }
91 players
92}
93
94fn unique_push_spell(spells: &mut Vec<SpellAbility>, spell: SpellAbility) {
95 let spell_source = spell.source;
96 let spell_api = spell.api;
97 let spell_text = spell.ability_text.clone();
98 let spell_target = spell.target_chosen.target_stack_entry;
99 if spells.iter().any(|existing| {
100 existing.source == spell_source
101 && existing.api == spell_api
102 && existing.ability_text == spell_text
103 && existing.target_chosen.target_stack_entry == spell_target
104 }) {
105 return;
106 }
107 spells.push(spell);
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, strum_macros::EnumString)]
116#[strum(ascii_case_insensitive)]
117pub enum DefinedCardToken {
118 #[strum(serialize = "Self", serialize = "CARDNAME")]
119 SelfCard,
120 Remembered,
121 #[strum(
122 serialize = "Enchanted",
123 serialize = "Equipped",
124 serialize = "AttachedTo"
125 )]
126 Attached,
127 EnchantedBy,
128 Imprinted,
129 #[strum(serialize = "TopOfLibrary", serialize = "OfLibrary")]
130 TopOfLibrary,
131 TopOfGraveyard,
132 Tapped,
133 Untapped,
134}
135
136pub fn get_defined_cards(
139 game: &GameState,
140 host_card: Option<CardId>,
141 defined: &str,
142 activating_player: Option<PlayerId>,
143) -> Vec<CardId> {
144 if let Ok(token) = defined.parse::<DefinedCardToken>() {
145 return resolve_defined_card_token(token, game, host_card, activating_player);
146 }
147 if let Some(rest) = defined.strip_prefix("ValidGraveyard") {
149 let filter = rest.trim();
150 let player = activating_player.unwrap_or_else(|| {
151 host_card
152 .map(|c| game.card(c).controller)
153 .unwrap_or(PlayerId(0))
154 });
155 return game
156 .cards_in_zone(ZoneType::Graveyard, player)
157 .iter()
158 .copied()
159 .filter(|&cid| {
160 if filter.is_empty() {
161 true
162 } else if let Some(source_id) = host_card {
163 matches_valid_cards_for_source(game, source_id, game.card(cid), None, filter)
164 } else {
165 matches_valid_cards(game.card(cid), filter, player)
166 }
167 })
168 .collect();
169 }
170 Vec::new()
174}
175
176fn resolve_defined_card_token(
177 token: DefinedCardToken,
178 game: &GameState,
179 host_card: Option<CardId>,
180 activating_player: Option<PlayerId>,
181) -> Vec<CardId> {
182 match token {
183 DefinedCardToken::SelfCard => host_card.into_iter().collect(),
184 DefinedCardToken::Remembered => host_card
185 .map(|src| game.card(src).remembered_cards.clone())
186 .unwrap_or_default(),
187 DefinedCardToken::Attached => host_card
188 .and_then(|src| game.card(src).attached_to)
189 .into_iter()
190 .collect(),
191 DefinedCardToken::EnchantedBy => host_card
192 .into_iter()
193 .flat_map(|src| {
194 game.cards
195 .iter()
196 .filter_map(move |c| {
197 (c.attached_to == Some(src) && c.type_line.has_subtype("Aura"))
198 .then_some(c.id)
199 })
200 .collect::<Vec<_>>()
201 })
202 .collect(),
203 DefinedCardToken::Imprinted => host_card
204 .map(|src| game.card(src).imprinted_cards.clone())
205 .unwrap_or_default(),
206 DefinedCardToken::TopOfLibrary => activating_player
207 .and_then(|pid| game.cards_in_zone(ZoneType::Library, pid).last().copied())
208 .into_iter()
209 .collect(),
210 DefinedCardToken::TopOfGraveyard => activating_player
211 .and_then(|pid| game.cards_in_zone(ZoneType::Graveyard, pid).last().copied())
212 .into_iter()
213 .collect(),
214 DefinedCardToken::Tapped => activating_player
215 .map(|pid| filter_battlefield(game, pid, |c| c.tapped))
216 .unwrap_or_default(),
217 DefinedCardToken::Untapped => activating_player
218 .map(|pid| filter_battlefield(game, pid, |c| !c.tapped))
219 .unwrap_or_default(),
220 }
221}
222
223fn filter_battlefield(
226 game: &GameState,
227 _starter: PlayerId,
228 pred: impl Fn(&Card) -> bool,
229) -> Vec<CardId> {
230 let mut out = Vec::new();
231 for &pid in &game.player_order {
232 for &cid in game.cards_in_zone(ZoneType::Battlefield, pid) {
233 if pred(game.card(cid)) {
234 out.push(cid);
235 }
236 }
237 }
238 out
239}
240
241pub fn get_defined_players(
244 game: &GameState,
245 _host_card: Option<CardId>,
246 defined: &str,
247 activating_player: Option<PlayerId>,
248) -> Vec<PlayerId> {
249 if let Some(player) = activating_player {
250 resolve_defined_players(defined, player, game)
251 } else {
252 Vec::new()
253 }
254}
255
256pub fn calculate_amount(value: &str) -> i32 {
262 value.parse::<i32>().unwrap_or(0)
263}
264
265pub fn parse_param(ability: &str, prefix: &str) -> Option<i32> {
269 for part in ability.split('|') {
270 let part = part.trim();
271 if let Some(val) = part.strip_prefix(prefix) {
272 if let Ok(n) = val.trim().parse::<i32>() {
273 return Some(n);
274 }
275 }
276 }
277 None
278}
279
280pub fn parse_num_dmg(ability: &str) -> i32 {
282 parse_param(ability, "NumDmg$ ").unwrap_or(0)
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Eq, strum_macros::EnumString)]
296#[strum(ascii_case_insensitive)]
297pub enum DefinedPlayerToken {
298 You,
299 #[strum(serialize = "Opponent", serialize = "OpponentCtrl")]
300 Opponent,
301 #[strum(
302 serialize = "DefendingPlayer",
303 serialize = "TriggeredDefendingPlayer",
304 serialize = "TriggeredDefender"
305 )]
306 DefendingPlayer,
307}
308
309pub fn resolve_defined_player(
310 defined: &str,
311 controller: PlayerId,
312 game: &GameState,
313) -> Option<PlayerId> {
314 let key = defined.strip_prefix("Player.").unwrap_or(defined);
316 let token = key.parse::<DefinedPlayerToken>().ok()?;
317 Some(match token {
318 DefinedPlayerToken::You => controller,
319 DefinedPlayerToken::Opponent | DefinedPlayerToken::DefendingPlayer => {
320 game.opponent_of(controller)
321 }
322 })
323}
324
325pub fn resolve_defined_player_with_sa(
328 defined: &str,
329 sa: &SpellAbility,
330 controller: PlayerId,
331 game: &GameState,
332) -> Option<PlayerId> {
333 fn parse_player_object(sa: &SpellAbility, key: AbilityKey) -> Option<PlayerId> {
334 sa.get_triggering_players(key).into_iter().next()
335 }
336
337 fn triggered_controller(
338 sa: &SpellAbility,
339 game: &GameState,
340 key: AbilityKey,
341 ) -> Option<PlayerId> {
342 sa.get_triggering_cards(key)
345 .into_iter()
346 .next()
347 .map(|cid| game.card(cid).controller)
348 .or_else(|| parse_player_object(sa, key))
349 }
350
351 fn triggered_owner(sa: &SpellAbility, game: &GameState, key: AbilityKey) -> Option<PlayerId> {
352 sa.get_triggering_cards(key)
353 .into_iter()
354 .next()
355 .map(|cid| game.card(cid).owner)
356 }
357
358 let key = defined.strip_prefix("Player.").unwrap_or(defined);
359 if let Some(rest) = key.strip_prefix("Non") {
360 return game.alive_players().into_iter().find(|pid| {
361 !resolve_defined_players_with_sa(rest, sa, controller, game).contains(pid)
362 });
363 }
364 match key {
365 _ if key.starts_with("Remembered") => {
366 remembered_players_for_def(key, sa, game).into_iter().next()
367 }
368 "TriggeredPlayer" | "Targeted" | "TargetedPlayer" => sa
369 .target_chosen
370 .all_target_players()
371 .into_iter()
372 .next()
373 .or_else(|| parse_player_object(sa, AbilityKey::Player)),
374 "ParentTarget" => sa.target_chosen.all_target_players().into_iter().next(),
375 "ThisTargetedPlayer" => sa.target_chosen.all_target_players().into_iter().next(),
376 "TargetedOrController" => sa
377 .target_chosen
378 .all_target_players()
379 .into_iter()
380 .next()
381 .or_else(|| targeted_controller_players(sa, game).into_iter().next()),
382 "TriggeredTarget" | "TriggeredTargets" => {
383 if let Some(player) = parse_player_object(sa, AbilityKey::TargetPlayer) {
384 Some(player)
385 } else if let Some(card) = sa
386 .get_triggering_cards(AbilityKey::TargetCard)
387 .into_iter()
388 .next()
389 {
390 Some(game.card(card).controller)
391 } else {
392 sa.get_triggering_cards(AbilityKey::Target)
395 .into_iter()
396 .next()
397 .map(|cid| game.card(cid).controller)
398 .or_else(|| parse_player_object(sa, AbilityKey::Target))
399 }
400 }
401 "TriggeredTargetController" | "TriggeredTargetsController" => {
402 if let Some(card) = sa
403 .get_triggering_cards(AbilityKey::TargetCard)
404 .into_iter()
405 .next()
406 {
407 Some(game.card(card).controller)
408 } else if let Some(player) = parse_player_object(sa, AbilityKey::TargetPlayer) {
409 Some(player)
410 } else {
411 sa.get_triggering_cards(AbilityKey::Target)
412 .into_iter()
413 .next()
414 .map(|cid| game.card(cid).controller)
415 .or_else(|| parse_player_object(sa, AbilityKey::Target))
416 }
417 }
418 "TriggeredAttackedTarget" => parse_player_object(sa, AbilityKey::AttackedTarget),
419 "TriggeredDefender" => parse_player_object(sa, AbilityKey::Defender)
420 .or_else(|| parse_player_object(sa, AbilityKey::AttackedTarget))
421 .or_else(|| parse_player_object(sa, AbilityKey::DefendingPlayer)),
422 "TriggeredAttackingPlayer" => parse_player_object(sa, AbilityKey::AttackingPlayer),
423 "TriggeredActivator" => parse_player_object(sa, AbilityKey::Activator),
424 "TriggeredOpponentVotedDiff" => parse_player_object(sa, AbilityKey::OpponentVotedDiff),
425 "TriggeredOpponentVotedSame" => parse_player_object(sa, AbilityKey::OpponentVotedSame),
426 "TriggeredCardController" => triggered_controller(sa, game, AbilityKey::Card),
427 "TriggeredCardOwner" => triggered_owner(sa, game, AbilityKey::Card),
428 "ReplacedCardController" => triggered_controller(sa, game, AbilityKey::ReplacedCard)
429 .or_else(|| triggered_controller(sa, game, AbilityKey::Card)),
430 "ReplacedCardOwner" => triggered_owner(sa, game, AbilityKey::ReplacedCard)
431 .or_else(|| triggered_owner(sa, game, AbilityKey::Card)),
432 "TriggeredSourceController" => triggered_controller(sa, game, AbilityKey::Source),
433 "TriggeredSourceSAController" => sa
434 .get_triggering_spell_ability("SourceSA")
435 .map(|cause_sa| cause_sa.activating_player),
436 "TriggeredPlayerController" => triggered_controller(sa, game, AbilityKey::Player),
437 "DefendingPlayer" | "TriggeredDefendingPlayer" => sa
438 .target_chosen
439 .target_player
440 .or_else(|| Some(game.opponent_of(controller))),
441 "TriggeredController" => sa
442 .trigger_source
443 .map(|cid| game.card(cid).controller)
444 .or(sa.target_chosen.target_player),
445 "TargetedController" | "ThisTargetedController" | "ParentTargetedController" => {
446 targeted_controller_players(sa, game).into_iter().next()
447 }
448 "TargetedOwner" | "ThisTargetedOwner" => {
449 targeted_owner_players(sa, game).into_iter().next()
450 }
451 _ => resolve_defined_player(key, controller, game),
452 }
453}
454
455pub fn resolve_defined_players_with_sa(
457 defined: &str,
458 sa: &SpellAbility,
459 controller: PlayerId,
460 game: &GameState,
461) -> Vec<PlayerId> {
462 let key = defined.strip_prefix("Player.").unwrap_or(defined);
463 if key.contains(" & ") {
464 let mut players = Vec::new();
465 for part in key
466 .split(" & ")
467 .map(str::trim)
468 .filter(|part| !part.is_empty())
469 {
470 for pid in resolve_defined_players_with_sa(part, sa, controller, game) {
471 push_unique_player(&mut players, pid);
472 }
473 }
474 return players;
475 }
476 if let Some(rest) = key.strip_prefix("Non") {
477 let excluded = resolve_defined_players_with_sa(rest, sa, controller, game);
478 return game
479 .alive_players()
480 .into_iter()
481 .filter(|pid| !excluded.contains(pid))
482 .collect();
483 }
484 match key {
485 "Player" | "Players" => game.alive_players(),
486 "DelayTriggerRemembered" | "TriggerRemembered" => {
491 let mut players = Vec::new();
492 for value in &sa.trigger_remembered {
493 match value {
494 crate::event::AbilityValue::Player(pid) => {
495 push_unique_player(&mut players, *pid)
496 }
497 crate::event::AbilityValue::Players(list) => {
498 for pid in list {
499 push_unique_player(&mut players, *pid);
500 }
501 }
502 _ => {}
503 }
504 }
505 players
506 }
507 _ if key.starts_with("Remembered") => remembered_players_for_def(key, sa, game),
508 "TriggeredPlayer" | "Targeted" | "TargetedPlayer" => {
509 let mut players = Vec::new();
510 for player in sa.target_chosen.all_target_players() {
511 push_unique_player(&mut players, player);
512 }
513 for player in sa.get_triggering_players(AbilityKey::Player) {
514 push_unique_player(&mut players, player);
515 }
516 players
517 }
518 "ParentTarget" => sa.target_chosen.all_target_players(),
519 "ThisTargetedPlayer" => sa.target_chosen.all_target_players(),
520 "TargetedOrController" => {
521 let mut players = sa.target_chosen.all_target_players();
522 for player in targeted_controller_players(sa, game) {
523 push_unique_player(&mut players, player);
524 }
525 players
526 }
527 "TargetedController" | "ThisTargetedController" | "ParentTargetedController" => {
528 targeted_controller_players(sa, game)
529 }
530 "TargetedOwner" | "ThisTargetedOwner" => targeted_owner_players(sa, game),
531 "TriggeredTarget" | "TriggeredTargets" => {
532 let mut players = Vec::new();
533 let target_players = sa.get_triggering_players(AbilityKey::TargetPlayer);
534 let target_cards = sa.get_triggering_cards(AbilityKey::TargetCard);
535 if !target_players.is_empty() {
536 for player in target_players {
537 push_unique_player(&mut players, player);
538 }
539 } else if !target_cards.is_empty() {
540 for cid in target_cards {
541 push_unique_player(&mut players, game.card(cid).controller);
542 }
543 } else {
544 let target_cards_fallback = sa.get_triggering_cards(AbilityKey::Target);
547 if !target_cards_fallback.is_empty() {
548 for cid in target_cards_fallback {
549 push_unique_player(&mut players, game.card(cid).controller);
550 }
551 } else {
552 for player in sa.get_triggering_players(AbilityKey::Target) {
553 push_unique_player(&mut players, player);
554 }
555 }
556 }
557 players
558 }
559 "TriggeredTargetController" | "TriggeredTargetsController" => {
560 let mut players = Vec::new();
561 let target_players = sa.get_triggering_players(AbilityKey::TargetPlayer);
562 let target_cards = sa.get_triggering_cards(AbilityKey::TargetCard);
563 if !target_cards.is_empty() {
564 for cid in target_cards {
565 push_unique_player(&mut players, game.card(cid).controller);
566 }
567 } else if !target_players.is_empty() {
568 for player in target_players {
569 push_unique_player(&mut players, player);
570 }
571 } else {
572 let target_cards_fallback = sa.get_triggering_cards(AbilityKey::Target);
574 if !target_cards_fallback.is_empty() {
575 for cid in target_cards_fallback {
576 push_unique_player(&mut players, game.card(cid).controller);
577 }
578 } else {
579 for player in sa.get_triggering_players(AbilityKey::Target) {
580 push_unique_player(&mut players, player);
581 }
582 }
583 }
584 players
585 }
586 "TriggeredAttackedTarget" => sa.get_triggering_players(AbilityKey::AttackedTarget),
587 "TriggeredDefender" => {
588 let mut players = sa.get_triggering_players(AbilityKey::Defender);
589 for pid in sa.get_triggering_players(AbilityKey::AttackedTarget) {
590 push_unique_player(&mut players, pid);
591 }
592 for pid in sa.get_triggering_players(AbilityKey::DefendingPlayer) {
593 push_unique_player(&mut players, pid);
594 }
595 players
596 }
597 "TriggeredAttackedTargetAndYou" => {
598 let mut players = sa.get_triggering_players(AbilityKey::AttackedTarget);
599 push_unique_player(&mut players, controller);
600 players
601 }
602 "TriggeredAttackingPlayer" => sa.get_triggering_players(AbilityKey::AttackingPlayer),
603 "TriggeredActivator" => sa.get_triggering_players(AbilityKey::Activator),
604 "TriggeredOpponentVotedDiff" => sa.get_triggering_players(AbilityKey::OpponentVotedDiff),
605 "TriggeredOpponentVotedSame" => sa.get_triggering_players(AbilityKey::OpponentVotedSame),
606 "TriggeredCardController" => sa
607 .get_triggering_cards(AbilityKey::Card)
608 .into_iter()
609 .map(|cid| game.card(cid).controller)
610 .collect(),
611 "TriggeredCardOwner" => sa
612 .get_triggering_cards(AbilityKey::Card)
613 .into_iter()
614 .map(|cid| game.card(cid).owner)
615 .collect(),
616 "ReplacedCardController" => {
617 let replaced = sa.get_triggering_cards(AbilityKey::ReplacedCard);
618 let cards = if replaced.is_empty() {
619 sa.get_triggering_cards(AbilityKey::Card)
620 } else {
621 replaced
622 };
623 cards
624 .into_iter()
625 .map(|cid| game.card(cid).controller)
626 .collect()
627 }
628 "ReplacedCardOwner" => {
629 let replaced = sa.get_triggering_cards(AbilityKey::ReplacedCard);
630 let cards = if replaced.is_empty() {
631 sa.get_triggering_cards(AbilityKey::Card)
632 } else {
633 replaced
634 };
635 cards.into_iter().map(|cid| game.card(cid).owner).collect()
636 }
637 "TriggeredSourceController" => sa
638 .get_triggering_cards(AbilityKey::Source)
639 .into_iter()
640 .map(|cid| game.card(cid).controller)
641 .collect(),
642 "TriggeredSourceSAController" => sa
643 .get_triggering_spell_ability("SourceSA")
644 .map(|cause_sa| vec![cause_sa.activating_player])
645 .unwrap_or_default(),
646 "TriggeredPlayerController" => {
647 let mut players = sa.get_triggering_players(AbilityKey::Player);
648 for cid in sa.get_triggering_cards(AbilityKey::Player) {
649 push_unique_player(&mut players, game.card(cid).controller);
650 }
651 players
652 }
653 "DefendingPlayer" | "TriggeredDefendingPlayer" => {
654 let mut players: Vec<_> = sa.target_chosen.target_player.into_iter().collect();
655 let defending = game.opponent_of(controller);
656 push_unique_player(&mut players, defending);
657 players
658 }
659 _ => resolve_defined_players(key, controller, game),
660 }
661}
662
663pub fn get_defined_spell_abilities(
667 defined: &str,
668 sa: &SpellAbility,
669 game: &GameState,
670) -> Vec<SpellAbility> {
671 let key = defined.trim();
672 let mut spells = Vec::new();
673
674 match key {
675 "Self" => unique_push_spell(&mut spells, sa.clone()),
677
678 "Parent" => unique_push_spell(&mut spells, sa.clone()),
686
687 "Remembered" => {
692 if let Some(host_id) = sa.source {
693 let remembered = game.card(host_id).remembered_cards.clone();
694 for cid in remembered {
695 for ab_text in game.card(cid).abilities.iter() {
696 let built = crate::spellability::build_spell_ability_from_host_card(
697 game.card(cid),
698 ab_text,
699 sa.activating_player,
700 );
701 unique_push_spell(&mut spells, built);
702 }
703 }
704 }
705 }
706
707 "Imprinted" => {
709 if let Some(host_id) = sa.source {
710 let imprinted = game.card(host_id).imprinted_cards.clone();
711 for cid in imprinted {
712 for ab_text in game.card(cid).abilities.iter() {
713 let built = crate::spellability::build_spell_ability_from_host_card(
714 game.card(cid),
715 ab_text,
716 sa.activating_player,
717 );
718 unique_push_spell(&mut spells, built);
719 }
720 }
721 }
722 }
723
724 "EffectSource" => {
730 if let Some(host_id) = sa.source {
731 if let Some(effect_src) = game.card(host_id).effect_source {
732 let src_card = game.card(effect_src);
733 if let Some(ab_text) = src_card.abilities.first() {
734 let built = crate::spellability::build_spell_ability_from_host_card(
735 src_card,
736 ab_text,
737 sa.activating_player,
738 );
739 unique_push_spell(&mut spells, built);
740 }
741 }
742 }
743 }
744
745 "SourceFirstSpell" => {
748 if let Some(host_id) = sa.source {
749 if let Some(entry) = game
750 .stack
751 .iter()
752 .find(|e| e.spell_ability.source == Some(host_id))
753 {
754 unique_push_spell(&mut spells, entry.spell_ability.clone());
755 }
756 }
757 }
758
759 "TriggeredSpellAbility" => {
760 for trigger_key in ["SpellAbility", "SourceSA", "Cause", "AbilityMana"] {
761 if let Some(spell) = sa.get_triggering_spell_ability(trigger_key) {
762 unique_push_spell(&mut spells, spell.clone());
763 }
764 }
765 }
766 "SpellTargeted" | "ThisTargeted" => {
767 if let Some(stack_id) = sa.target_chosen.target_stack_entry {
768 if let Some(entry) = game.stack.find_by_id(stack_id) {
769 unique_push_spell(&mut spells, entry.spell_ability.clone());
770 }
771 }
772 }
773 "SourceSA" | "SpellAbility" | "AbilityMana" | "Cause" | "StackSa" => {
774 if let Some(spell) = sa.get_triggering_spell_ability(key) {
775 unique_push_spell(&mut spells, spell.clone());
776 }
777 }
778 "TopStack" => {
779 if let Some(spell) = game.stack.peek_ability() {
780 unique_push_spell(&mut spells, spell.clone());
781 }
782 }
783 _ => {
784 if let Some(trigger_key) = key.strip_prefix("Triggered") {
787 if let Some(spell) = sa.get_triggering_spell_ability(trigger_key) {
788 unique_push_spell(&mut spells, spell.clone());
789 }
790 }
791 }
792 }
793
794 spells
795}
796
797pub fn resolve_defined_players(
801 defined: &str,
802 controller: PlayerId,
803 game: &GameState,
804) -> Vec<PlayerId> {
805 if defined.contains(" & ") {
806 let mut players = Vec::new();
807 for part in defined
808 .split(" & ")
809 .map(str::trim)
810 .filter(|part| !part.is_empty())
811 {
812 for pid in resolve_defined_players(part, controller, game) {
813 if !players.contains(&pid) {
814 players.push(pid);
815 }
816 }
817 }
818 return players;
819 }
820 if let Some(rest) = defined.strip_prefix("Non") {
821 let excluded = resolve_defined_players(rest, controller, game);
822 return game
823 .alive_players()
824 .into_iter()
825 .filter(|pid| !excluded.contains(pid))
826 .collect();
827 }
828 match defined {
829 "You" => vec![controller],
830 "Opponent" | "OpponentCtrl" => vec![game.opponent_of(controller)],
831 "DefendingPlayer" | "TriggeredDefendingPlayer" | "TriggeredDefender" => {
832 vec![game.opponent_of(controller)]
833 }
834 "Each" | "All" | "Player" => game.alive_players(),
835 _ => {
836 if let Some(pid) = resolve_defined_player(defined, controller, game) {
838 vec![pid]
839 } else {
840 vec![controller]
841 }
842 }
843 }
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
850
851 use crate::card::Card;
852 use crate::ids::{CardId, PlayerId};
853
854 fn make_card(
855 game: &mut GameState,
856 owner: PlayerId,
857 controller: PlayerId,
858 name: &str,
859 ) -> CardId {
860 let mut card = Card::new(
861 CardId(0),
862 name.to_string(),
863 owner,
864 CardTypeLine::parse("Creature"),
865 ManaCost::parse("1"),
866 ColorSet::COLORLESS,
867 Some(1),
868 Some(1),
869 vec![],
870 vec![],
871 );
872 card.controller = controller;
873 game.create_card(card)
874 }
875
876 #[test]
877 fn targeted_controller_ignores_player_targets() {
878 let game = GameState::new(&["P0", "P1"], 20);
879 let p0 = PlayerId(0);
880 let p1 = PlayerId(1);
881
882 let mut sa = SpellAbility::new_simple(None, p0, "DB$ Discard");
883 sa.target_chosen.target_player = Some(p1);
884
885 assert_eq!(
886 resolve_defined_player_with_sa("TargetedController", &sa, p0, &game),
887 None
888 );
889 assert!(resolve_defined_players_with_sa("TargetedController", &sa, p0, &game).is_empty());
890 assert_eq!(
891 resolve_defined_players_with_sa("TargetedOrController", &sa, p0, &game),
892 vec![p1]
893 );
894 }
895
896 #[test]
897 fn targeted_controller_and_owner_use_targeted_card_only() {
898 let mut game = GameState::new(&["P0", "P1"], 20);
899 let p0 = PlayerId(0);
900 let p1 = PlayerId(1);
901 let target = make_card(&mut game, p0, p1, "Borrowed Bear");
902
903 let mut sa = SpellAbility::new_simple(None, p0, "DB$ Draw");
904 sa.target_chosen.target_card = Some(target);
905
906 assert_eq!(
907 resolve_defined_player_with_sa("TargetedController", &sa, p0, &game),
908 Some(p1)
909 );
910 assert_eq!(
911 resolve_defined_player_with_sa("TargetedOwner", &sa, p0, &game),
912 Some(p0)
913 );
914 assert_eq!(
915 resolve_defined_players_with_sa("TargetedController", &sa, p0, &game),
916 vec![p1]
917 );
918 assert_eq!(
919 resolve_defined_players_with_sa("TargetedOwner", &sa, p0, &game),
920 vec![p0]
921 );
922 }
923
924 #[test]
925 fn this_targeted_player_stays_on_current_sa_targets() {
926 let game = GameState::new(&["P0", "P1"], 20);
927 let p0 = PlayerId(0);
928 let p1 = PlayerId(1);
929
930 let mut sa = SpellAbility::new_simple(None, p0, "DB$ Token");
931 sa.target_chosen.target_player = Some(p1);
932
933 assert_eq!(
934 resolve_defined_players_with_sa("ThisTargetedPlayer", &sa, p0, &game),
935 vec![p1]
936 );
937 }
938}
939
940pub fn parse_counter_type(s: &str) -> CounterType {
947 crate::card::counter_type::parse_counter_type(s)
948}
949
950pub fn parse_zone_type(s: &str) -> Option<ZoneType> {
954 let s = s.trim();
955 if s.eq_ignore_ascii_case("Deck") {
956 Some(ZoneType::Library)
957 } else {
958 ZoneType::from_str_compat(s)
959 }
960}
961
962pub fn matches_valid_cards(card: &Card, filter: &str, activating_player: PlayerId) -> bool {
976 if filter.is_empty() || filter == fc::CARD {
977 return true;
978 }
979
980 if filter.contains(", ") {
982 return filter
983 .split(", ")
984 .any(|part| matches_valid_cards_single(card, part.trim(), activating_player));
985 }
986
987 matches_valid_cards_single(card, filter, activating_player)
988}
989
990pub fn matches_valid_cards_selector_opt(
991 selector: Option<&CompiledSelector>,
992 card: &Card,
993 activating_player: PlayerId,
994) -> bool {
995 selector.is_none_or(|selector| {
996 selector.alternatives.iter().any(|alternative| {
997 matches_valid_cards_single(card, &alternative.raw, activating_player)
998 })
999 })
1000}
1001
1002pub fn matches_valid_cards_for_sa(
1003 game: &GameState,
1004 sa: &SpellAbility,
1005 card: &Card,
1006 selector: Option<&CompiledSelector>,
1007 default_filter: &str,
1008) -> bool {
1009 match (selector, sa.source) {
1010 (Some(selector), Some(source_id)) => valid_filter::matches_valid_card_selector_in_game(
1011 selector,
1012 card,
1013 game.card(source_id),
1014 game,
1015 ),
1016 (Some(selector), None) => {
1017 matches_valid_cards_selector_opt(Some(selector), card, sa.activating_player)
1018 }
1019 (None, Some(source_id)) => valid_filter::matches_valid_card_selector_in_game(
1020 &cached_compiled_selector(default_filter),
1021 card,
1022 game.card(source_id),
1023 game,
1024 ),
1025 (None, None) => matches_valid_cards(card, default_filter, sa.activating_player),
1026 }
1027}
1028
1029pub fn matches_valid_cards_for_source(
1030 game: &GameState,
1031 source_id: CardId,
1032 card: &Card,
1033 selector: Option<&CompiledSelector>,
1034 default_filter: &str,
1035) -> bool {
1036 let source = game.card(source_id);
1037 let parsed;
1038 let selector = match selector {
1039 Some(selector) => selector,
1040 None => {
1041 parsed = cached_compiled_selector(default_filter);
1042 &parsed
1043 }
1044 };
1045 valid_filter::matches_valid_card_selector_in_game(selector, card, source, game)
1046}
1047
1048fn matches_valid_cards_single(card: &Card, filter: &str, activating_player: PlayerId) -> bool {
1049 let parts: Vec<&str> = filter.split('.').collect();
1050 let type_part = parts[0];
1051
1052 let type_matches = match type_part {
1054 fc::CREATURE => card.is_creature(),
1055 fc::LAND => card.is_land(),
1056 fc::ARTIFACT => card
1057 .type_line
1058 .core_types
1059 .iter()
1060 .any(|t| t.name().eq_ignore_ascii_case(fc::ARTIFACT)),
1061 fc::ENCHANTMENT => card
1062 .type_line
1063 .core_types
1064 .iter()
1065 .any(|t| t.name().eq_ignore_ascii_case(fc::ENCHANTMENT)),
1066 fc::PLANESWALKER => card
1067 .type_line
1068 .core_types
1069 .iter()
1070 .any(|t| t.name().eq_ignore_ascii_case(fc::PLANESWALKER)),
1071 fc::INSTANT => card
1072 .type_line
1073 .core_types
1074 .iter()
1075 .any(|t| t.name().eq_ignore_ascii_case(fc::INSTANT)),
1076 fc::SORCERY => card
1077 .type_line
1078 .core_types
1079 .iter()
1080 .any(|t| t.name().eq_ignore_ascii_case(fc::SORCERY)),
1081 fc::PERMANENT | fc::CARD => true,
1082 _ => true, };
1084 if !type_matches {
1085 return false;
1086 }
1087
1088 for &qualifier in &parts[1..] {
1091 let sub_parts: Vec<&str> = qualifier.split('+').collect();
1092 for sub in &sub_parts {
1093 if !matches_valid_cards_qualifier(card, sub, activating_player) {
1094 return false;
1095 }
1096 }
1097 }
1098 true
1099}
1100
1101fn matches_valid_cards_qualifier(
1102 card: &Card,
1103 qualifier: &str,
1104 activating_player: PlayerId,
1105) -> bool {
1106 match qualifier {
1107 fc::YOU_CTRL => card.controller == activating_player,
1108 fc::OPP_CTRL => card.controller != activating_player,
1109 fc::BASIC => card.type_line.is_basic(),
1110 fc::KICKED => card.kicked,
1111 fc::WITH_FLYING => {
1112 card.keywords.contains_string_ignore_case("Flying")
1113 || card.granted_keywords.contains_string_ignore_case("Flying")
1114 }
1115 _ => {
1116 if let Some(keyword) = qualifier.strip_prefix("without") {
1117 return keyword.is_empty() || !card.has_keyword(keyword);
1118 }
1119 if let Some(keyword) = qualifier.strip_prefix("with") {
1120 return !keyword.is_empty() && card.has_keyword(keyword);
1121 }
1122 if let Some(target) = qualifier.strip_prefix("attacking ") {
1124 let attacking = card.attacking_player;
1125 match target {
1126 "Opponent" => match attacking {
1127 Some(def) => def != activating_player,
1128 None => false,
1129 },
1130 "Planeswalker" => false,
1133 _ => attacking.is_some(), }
1135 }
1136 else {
1139 let lower = qualifier.to_ascii_lowercase();
1140 if let Some(rest) = lower.strip_prefix("non") {
1141 match rest {
1142 "land" => !card.is_land(),
1143 "creature" => !card.is_creature(),
1144 "artifact" => !card.type_line.is_artifact(),
1145 "enchantment" => !card.type_line.is_enchantment(),
1146 "planeswalker" => !card.type_line.is_planeswalker(),
1147 "token" => !card.is_token,
1148 "basic" => !card.type_line.is_basic(),
1149 _ => {
1150 let excluded = ColorSet::from_names(rest);
1151 !card.color.shares_color_with(excluded)
1152 }
1153 }
1154 } else {
1155 true
1157 }
1158 }
1159 }
1160 }
1161}
1162
1163pub fn matches_change_type(
1171 card: &Card,
1172 change_type: &str,
1173 source_chosen_colors: &[String],
1174) -> bool {
1175 if change_type.is_empty() {
1176 return true;
1177 }
1178
1179 if change_type.contains(';') {
1183 return change_type
1184 .split(';')
1185 .any(|alt| matches_change_type(card, alt.trim(), source_chosen_colors));
1186 }
1187
1188 if change_type.contains(',') {
1191 return change_type
1192 .split(',')
1193 .any(|alt| matches_change_type(card, alt.trim(), source_chosen_colors));
1194 }
1195
1196 let parts: Vec<&str> = change_type.split(['.', '+']).collect();
1200 let type_part = parts[0];
1201
1202 let type_matches = match type_part {
1203 fc::LAND => card.is_land(),
1204 fc::CREATURE => card.is_creature(),
1205 fc::ARTIFACT => card.type_line.is_artifact(),
1206 fc::ENCHANTMENT => card.type_line.is_enchantment(),
1207 fc::INSTANT => card.type_line.is_instant(),
1208 fc::SORCERY => card.type_line.is_sorcery(),
1209 fc::PLANESWALKER => card.type_line.is_planeswalker(),
1210 fc::PERMANENT => card.is_permanent(),
1211 fc::CARD => true,
1212 "Plains" | "Island" | "Swamp" | "Mountain" | "Forest" => card
1215 .type_line
1216 .subtypes
1217 .iter()
1218 .any(|st| st.eq_ignore_ascii_case(type_part)),
1219 _ => card.has_subtype(type_part),
1220 };
1221
1222 if !type_matches {
1223 return false;
1224 }
1225
1226 for &qualifier in &parts[1..] {
1227 match qualifier {
1228 q if q.starts_with("named") => {
1229 let expected = q[5..].replace(';', ",").replace('_', " ");
1230 if !card.card_name.eq_ignore_ascii_case(&expected) {
1231 return false;
1232 }
1233 }
1234 fc::BASIC => {
1235 if !card.type_line.is_basic() {
1236 return false;
1237 }
1238 }
1239 fc::NON_LAND => {
1240 if card.is_land() {
1241 return false;
1242 }
1243 }
1244 fc::ATTACKING => {
1245 if card.attacking_player.is_none() {
1246 return false;
1247 }
1248 }
1249 "Red" => {
1250 if !card.color.has_red() {
1251 return false;
1252 }
1253 }
1254 "White" => {
1255 if !card.color.has_white() {
1256 return false;
1257 }
1258 }
1259 "Blue" => {
1260 if !card.color.has_blue() {
1261 return false;
1262 }
1263 }
1264 "Black" => {
1265 if !card.color.has_black() {
1266 return false;
1267 }
1268 }
1269 "Green" => {
1270 if !card.color.has_green() {
1271 return false;
1272 }
1273 }
1274 "Colorless" => {
1275 if !card.color.is_colorless() {
1276 return false;
1277 }
1278 }
1279 "Other" => {}
1282 "YouCtrl" | "YouControl" | "You" | "YouOwn" | "OppCtrl" | "OpponentCtrl" | "OppOwn"
1288 | "OpponentOwn" | "Opponent" => {}
1289 "Token" => {
1291 if !card.is_token {
1292 return false;
1293 }
1294 }
1295 "nonToken" => {
1296 if card.is_token {
1297 return false;
1298 }
1299 }
1300 "Legendary" => {
1302 if !card.type_line.is_legendary() {
1303 return false;
1304 }
1305 }
1306 "nonLegendary" => {
1307 if card.type_line.is_legendary() {
1308 return false;
1309 }
1310 }
1311 "Snow" => {
1312 if !card.type_line.is_snow() {
1313 return false;
1314 }
1315 }
1316 "nonBasic" => {
1317 if card.type_line.is_basic() {
1318 return false;
1319 }
1320 }
1321 "nonCreature" => {
1322 if card.is_creature() {
1323 return false;
1324 }
1325 }
1326 "nonArtifact" => {
1327 if card.type_line.is_artifact() {
1328 return false;
1329 }
1330 }
1331 "nonEnchantment" => {
1332 if card.type_line.is_enchantment() {
1333 return false;
1334 }
1335 }
1336 q if q.starts_with("cmc") && q.len() > 5 => {
1343 let op = &q[3..5];
1344 let rest = &q[5..];
1345 let mana_value = card.mana_value();
1346 let pass = if let Ok(n) = rest.parse::<i32>() {
1347 match op {
1348 "EQ" => mana_value == n,
1349 "LE" => mana_value <= n,
1350 "GE" => mana_value >= n,
1351 "LT" => mana_value < n,
1352 "GT" => mana_value > n,
1353 _ => true,
1354 }
1355 } else {
1356 true
1357 };
1358 if !pass {
1359 return false;
1360 }
1361 }
1362 q if q.starts_with("power") && q.len() > 7 => {
1363 if !matches_numeric_qualifier(card.power(), &q[5..]) {
1364 return false;
1365 }
1366 }
1367 q if q.starts_with("toughness") && q.len() > 11 => {
1368 if !matches_numeric_qualifier(card.toughness(), &q[9..]) {
1369 return false;
1370 }
1371 }
1372 "ChosenColor" => {
1373 if source_chosen_colors.is_empty() {
1374 return false;
1375 }
1376 let mut chosen_set = ColorSet::COLORLESS;
1377 for name in source_chosen_colors {
1378 chosen_set = chosen_set.union(ColorSet::from_names(name));
1379 }
1380 if !card.color.shares_color_with(chosen_set) {
1381 return false;
1382 }
1383 }
1384 other => {
1392 if let Some(stripped) = other.strip_prefix("non") {
1393 let is_creature_type = !stripped.is_empty();
1394 if !is_creature_type {
1395 return false;
1396 }
1397 let has_sub = card.type_line.has_subtype(stripped);
1398 let changeling_match = card.is_creature() && card.has_keyword("Changeling");
1399 if has_sub || changeling_match {
1400 return false;
1401 }
1402 continue;
1403 }
1404 let has_sub = card.type_line.has_subtype(other);
1405 let changeling_match = card.is_creature() && card.has_keyword("Changeling");
1406 if !has_sub && !changeling_match {
1407 return false;
1408 }
1409 }
1410 }
1411 }
1412
1413 true
1414}
1415
1416fn matches_numeric_qualifier(actual: i32, comparison: &str) -> bool {
1417 if comparison.len() < 3 {
1418 return true;
1419 }
1420 let op = &comparison[..2];
1421 let rest = &comparison[2..];
1422 if let Ok(expected) = rest.parse::<i32>() {
1423 match op {
1424 "EQ" => actual == expected,
1425 "LE" => actual <= expected,
1426 "GE" => actual >= expected,
1427 "LT" => actual < expected,
1428 "GT" => actual > expected,
1429 "NE" => actual != expected,
1430 _ => true,
1431 }
1432 } else {
1433 true
1434 }
1435}
1436
1437pub fn x_count(game: &GameState, card_id: CardId, expr: &str, sa: &SpellAbility) -> i32 {
1445 let controller = game.card(card_id).controller;
1446 if let Some(rest) = expr.strip_prefix("Number$") {
1448 let parts: Vec<&str> = rest.split('/').collect();
1449 let base = parts[0].trim().parse::<i32>().unwrap_or(0);
1450 let operators = parts.get(1).copied().unwrap_or("");
1451 return do_x_math(base, operators);
1452 }
1453
1454 let stripped = expr.strip_prefix("Count$").unwrap_or(expr);
1456
1457 if let Some(svar_name) = stripped.strip_prefix("SVar$") {
1459 if let Some(svar_val) = game.card(card_id).get_s_var(svar_name.trim()) {
1460 let val = svar_val;
1461 return x_count(game, card_id, val, sa);
1462 }
1463 return 0;
1464 }
1465
1466 let full_expr = if expr.starts_with("Count$") || expr.starts_with("Number$") {
1468 expr.to_string()
1469 } else {
1470 format!("Count${expr}")
1471 };
1472 crate::svar::resolve_count_svar_for_sa(&full_expr, game, card_id, controller, sa)
1473}
1474
1475pub fn do_x_math(num: i32, operators: &str) -> i32 {
1482 if operators.is_empty() || operators == "none" {
1483 return num;
1484 }
1485 let parts: Vec<&str> = operators.split('.').collect();
1487 let op = parts.first().copied().unwrap_or("");
1488 let secondary = parts
1489 .get(1)
1490 .and_then(|s| s.parse::<i32>().ok())
1491 .unwrap_or(0);
1492
1493 if op.contains("Plus") {
1494 num + secondary
1495 } else if op.contains("NMinus") {
1496 secondary - num
1497 } else if op.contains("Minus") {
1498 num - secondary
1499 } else if op.contains("Twice") {
1500 num * 2
1501 } else if op.contains("Thrice") {
1502 num * 3
1503 } else if op.contains("HalfUp") {
1504 ((num as f64) / 2.0).ceil() as i32
1505 } else if op.contains("HalfDown") {
1506 ((num as f64) / 2.0).floor() as i32
1507 } else if op.contains("ThirdUp") {
1508 ((num as f64) / 3.0).ceil() as i32
1509 } else if op.contains("ThirdDown") {
1510 ((num as f64) / 3.0).floor() as i32
1511 } else if op.contains("Negative") {
1512 -num
1513 } else if op.contains("Times") {
1514 num * secondary
1515 } else if op.contains("DivideEvenlyUp") {
1516 if secondary == 0 {
1517 0
1518 } else {
1519 num / secondary + i32::from(num % secondary != 0)
1520 }
1521 } else if op.contains("DivideEvenlyDown") {
1522 if secondary == 0 {
1523 0
1524 } else {
1525 num / secondary
1526 }
1527 } else if op.contains("Abs") {
1528 num.abs()
1529 } else if op.contains("LimitMax") {
1530 num.min(secondary)
1531 } else if op.contains("LimitMin") {
1532 num.max(secondary)
1533 } else {
1534 num
1535 }
1536}
1537
1538pub fn player_x_count(
1544 game: &GameState,
1545 players: &[PlayerId],
1546 expr: &str,
1547 source_id: CardId,
1548 sa: &SpellAbility,
1549) -> i32 {
1550 let parts: Vec<&str> = expr.split('/').collect();
1551 let property = parts[0].trim();
1552 let operators = parts.get(1).copied().unwrap_or("");
1553
1554 let base: i32 = players
1555 .iter()
1556 .map(|&pid| player_x_property(game, pid, property, source_id, sa))
1557 .sum();
1558
1559 do_x_math(base, operators)
1560}
1561
1562pub fn player_x_property(
1565 game: &GameState,
1566 player: PlayerId,
1567 property: &str,
1568 _source_id: CardId,
1569 _sa: &SpellAbility,
1570) -> i32 {
1571 let p = game.player(player);
1572 match property {
1573 "LifeTotal" => p.life,
1574 "Poison" | "PoisonCounters" => p.poison_counters,
1575 "CardsInHand" => game.cards_in_zone(ZoneType::Hand, player).len() as i32,
1576 "CardsInLibrary" => game.cards_in_zone(ZoneType::Library, player).len() as i32,
1577 "CardsInGraveyard" | "GraveyardSize" => {
1578 game.cards_in_zone(ZoneType::Graveyard, player).len() as i32
1579 }
1580 "StartingLife" => p.starting_life,
1581 "LandsPlayedThisTurn" => p.lands_played_this_turn,
1582 "DiscardedThisTurn" | "NumDiscardedThisTurn" => p.discarded_this_turn,
1583 "NumRollsThisTurn" | "DiceRolledThisTurn" => p.num_rolls_this_turn,
1584 "AttractionsVisitedThisTurn" => p.attractions_visited_this_turn,
1585 "DomainCount" => {
1586 let lands = game.cards_in_zone(ZoneType::Battlefield, player);
1588 let mut types = std::collections::HashSet::new();
1589 for &cid in lands {
1590 let card = game.card(cid);
1591 if !card.is_land() {
1592 continue;
1593 }
1594 for subtype in &card.type_line.subtypes {
1595 let lower = subtype.to_ascii_lowercase();
1596 if matches!(
1597 lower.as_str(),
1598 "plains" | "island" | "swamp" | "mountain" | "forest"
1599 ) {
1600 types.insert(lower);
1601 }
1602 }
1603 }
1604 types.len() as i32
1605 }
1606 "NumCreaturesYouCtrl" | "CreatureCount" => game
1607 .cards_in_zone(ZoneType::Battlefield, player)
1608 .iter()
1609 .filter(|&&cid| game.card(cid).is_creature())
1610 .count() as i32,
1611 _ => 0,
1612 }
1613}
1614
1615pub fn object_x_count(game: &GameState, objects: &[CardId], expr: &str) -> i32 {
1620 let parts: Vec<&str> = expr.split('/').collect();
1621 let property = parts[0].trim();
1622 let operators = parts.get(1).copied().unwrap_or("");
1623
1624 let base: i32 = match property {
1625 "Amount" | "Count" => objects.len() as i32,
1626 "TotalPower" => objects.iter().map(|&cid| game.card(cid).power()).sum(),
1627 "TotalToughness" => objects.iter().map(|&cid| game.card(cid).toughness()).sum(),
1628 "TotalCMC" | "TotalManaValue" => {
1629 objects.iter().map(|&cid| game.card(cid).mana_value()).sum()
1630 }
1631 "SumPower" => objects.iter().map(|&cid| game.card(cid).power()).sum(),
1632 _ => objects.len() as i32,
1633 };
1634
1635 do_x_math(base, operators)
1636}
1637
1638pub fn handle_remembering(game: &mut GameState, sa: &SpellAbility) {
1646 let host_id = match sa.source {
1647 Some(id) => id,
1648 None => return,
1649 };
1650
1651 if sa.ir.remember_targets && sa.uses_targeting() {
1652 if sa.ir.forget_other_targets {
1653 game.card_mut(host_id).clear_remembered();
1654 }
1655 if let Some(target_card) = sa.target_chosen.target_card {
1656 game.card_mut(host_id).add_remembered_card(target_card);
1657 }
1658 if let Some(target_player) = sa.target_chosen.target_player {
1659 game.card_mut(host_id).add_remembered_player(target_player);
1660 }
1661 if let Some(stack_id) = sa.target_chosen.target_stack_entry {
1666 if let Some(entry) = game.stack.find_by_id(stack_id) {
1667 if let Some(source) = entry.spell_ability.source {
1668 game.card_mut(host_id).add_remembered_card(source);
1669 }
1670 }
1671 }
1672 }
1673
1674 if sa.ir.remember_cost_mana {
1678 game.card_mut(host_id).clear_remembered();
1679 }
1680}
1681
1682pub fn handle_paid(
1687 game: &GameState,
1688 paid_cards: &[CardId],
1689 property: &str,
1690 _source_id: CardId,
1691) -> i32 {
1692 if paid_cards.is_empty() {
1693 return 0;
1694 }
1695
1696 match property {
1697 "Amount" | "Count" => paid_cards.len() as i32,
1698 "CardPower" => paid_cards
1699 .iter()
1700 .map(|&cid| {
1701 let card = game.card(cid);
1702 card.lki_power.unwrap_or_else(|| card.power())
1703 })
1704 .sum(),
1705 "CardBasePower" => paid_cards
1706 .iter()
1707 .map(|&cid| game.card(cid).base_power.unwrap_or(0))
1708 .sum(),
1709 "CardToughness" => paid_cards
1710 .iter()
1711 .map(|&cid| {
1712 let card = game.card(cid);
1713 card.lki_toughness.unwrap_or_else(|| card.toughness())
1714 })
1715 .sum(),
1716 "CardSumPT" => paid_cards
1717 .iter()
1718 .map(|&cid| {
1719 let card = game.card(cid);
1720 card.lki_power.unwrap_or_else(|| card.power())
1721 + card.lki_toughness.unwrap_or_else(|| card.toughness())
1722 })
1723 .sum(),
1724 "CardManaCost" | "ManaCost" => paid_cards
1725 .iter()
1726 .map(|&cid| game.card(cid).mana_value())
1727 .sum(),
1728 "TotalPower" | "SumPower" => paid_cards
1729 .iter()
1730 .map(|&cid| {
1731 let card = game.card(cid);
1732 card.lki_power.unwrap_or(card.base_power.unwrap_or(0))
1733 })
1734 .sum(),
1735 "TotalToughness" | "SumToughness" => paid_cards
1736 .iter()
1737 .map(|&cid| {
1738 let card = game.card(cid);
1739 card.lki_toughness
1740 .unwrap_or(card.base_toughness.unwrap_or(0))
1741 })
1742 .sum(),
1743 "TotalCMC" | "SumCMC" => paid_cards
1744 .iter()
1745 .map(|&cid| game.card(cid).mana_value())
1746 .sum(),
1747 _ if property.starts_with("Valid ") => {
1748 let filter = property.strip_prefix("Valid ").unwrap_or("");
1749 paid_cards
1750 .iter()
1751 .filter(|&&cid| matches_change_type(game.card(cid), filter, &[]))
1752 .count() as i32
1753 }
1754 _ => paid_cards.len() as i32,
1755 }
1756}
1757
1758pub fn count_card_types_from_list(
1766 game: &GameState,
1767 cards: &[CardId],
1768 permanent_types: bool,
1769) -> i32 {
1770 let mut types = std::collections::HashSet::new();
1771 for &cid in cards {
1772 let card = game.card(cid);
1773 for ct in &card.type_line.core_types {
1774 types.insert(ct.name().to_string());
1775 }
1776 }
1777 if permanent_types {
1778 types
1779 .iter()
1780 .filter(|t| {
1781 matches!(
1782 t.as_str(),
1783 "Artifact"
1784 | "Creature"
1785 | "Enchantment"
1786 | "Land"
1787 | "Planeswalker"
1788 | "Battle"
1789 | "Kindred"
1790 )
1791 })
1792 .count() as i32
1793 } else {
1794 types.len() as i32
1795 }
1796}
1797
1798pub fn count_super_types_from_list(game: &GameState, cards: &[CardId]) -> i32 {
1801 let mut types = std::collections::HashSet::new();
1802 for &cid in cards {
1803 let card = game.card(cid);
1804 for st in &card.type_line.supertypes {
1805 types.insert(*st);
1806 }
1807 }
1808 types.len() as i32
1809}
1810
1811pub fn count_sub_types_from_list(game: &GameState, cards: &[CardId]) -> i32 {
1814 let mut types = std::collections::HashSet::new();
1815 for &cid in cards {
1816 let card = game.card(cid);
1817 for subtype in &card.type_line.subtypes {
1818 types.insert(subtype.clone());
1819 }
1820 }
1821 types.len() as i32
1822}
1823
1824pub fn calculate_unless_cost(
1832 game: &GameState,
1833 sa: &SpellAbility,
1834 unless_cost: &str,
1835) -> Option<crate::cost::Cost> {
1836 if unless_cost.is_empty() {
1837 return None;
1838 }
1839
1840 if unless_cost == "ChosenNumber" {
1842 if let Some(source_id) = sa.source {
1843 let chosen = game.card(source_id).chosen_number.unwrap_or(0);
1844 let mana_str = chosen.to_string();
1845 return Some(crate::cost::parse_cost(&mana_str));
1846 }
1847 return None;
1848 }
1849
1850 if let Some(source_id) = sa.source {
1852 if let Some(svar_val) = game.card(source_id).get_s_var(unless_cost.trim()) {
1853 if !svar_val.is_empty() && unless_cost != "X" {
1854 let amount = crate::svar::resolve_count_svar_for_sa(
1855 svar_val,
1856 game,
1857 source_id,
1858 sa.activating_player,
1859 sa,
1860 );
1861 let mana_str = amount.to_string();
1862 return Some(crate::cost::parse_cost(&mana_str));
1863 }
1864 }
1865 }
1866
1867 Some(crate::cost::parse_cost(unless_cost))
1869}
1870
1871pub fn filter_list_by_type(
1879 game: &GameState,
1880 cards: &[CardId],
1881 filter_type: &str,
1882 sa: &SpellAbility,
1883) -> Vec<CardId> {
1884 if filter_type.is_empty() {
1885 return cards.to_vec();
1886 }
1887
1888 let (effective_source, effective_filter) = if filter_type.starts_with("Triggered") {
1890 let trigger_card = sa
1892 .get_triggering_card(AbilityKey::Card)
1893 .or_else(|| sa.get_triggering_card(AbilityKey::Object))
1894 .or_else(|| sa.get_triggering_card(AbilityKey::Attacker))
1895 .or_else(|| sa.get_triggering_card(AbilityKey::Blocker));
1896
1897 match trigger_card {
1898 Some(cid) => {
1899 let adjusted = filter_type
1900 .replace("TriggeredCard", "Card")
1901 .replace("TriggeredObject", "Card")
1902 .replace("TriggeredAttacker", "Card")
1903 .replace("TriggeredBlocker", "Card")
1904 .replace("Triggered", "Card");
1905 (Some(cid), adjusted)
1906 }
1907 None => return Vec::new(),
1908 }
1909 } else if filter_type.starts_with("Targeted") {
1910 match sa.target_chosen.target_card {
1912 Some(cid) => {
1913 let adjusted = filter_type
1914 .replace("TargetedCard", "Card")
1915 .replace("Targeted", "Card");
1916 (Some(cid), adjusted)
1917 }
1918 None => return Vec::new(),
1919 }
1920 } else if filter_type.starts_with("Remembered") {
1921 let source_id = sa.source.unwrap_or(CardId(0));
1923 let remembered = &game.card(source_id).remembered_cards;
1924 match remembered.first() {
1925 Some(&cid) => {
1926 let adjusted = filter_type.replace("Remembered", "Card");
1927 (Some(cid), adjusted)
1928 }
1929 None => return Vec::new(),
1930 }
1931 } else {
1932 (sa.source, filter_type.to_string())
1933 };
1934
1935 let selector = cached_compiled_selector(&effective_filter);
1936 cards
1937 .iter()
1938 .copied()
1939 .filter(|&cid| {
1940 let card = game.card(cid);
1941 if effective_filter.contains(".Self") {
1943 if let Some(src) = effective_source {
1944 return cid == src;
1945 }
1946 }
1947 if let Some(source_id) = effective_source {
1948 matches_valid_cards_for_source(
1949 game,
1950 source_id,
1951 card,
1952 Some(&selector),
1953 &effective_filter,
1954 )
1955 } else {
1956 matches_valid_cards(card, &effective_filter, sa.activating_player)
1957 }
1958 })
1959 .collect()
1960}
1961
1962pub fn apply_mana_color_conversion(
1975 conversions: &mut std::collections::HashMap<String, Vec<String>>,
1976 conversion_str: &str,
1977) {
1978 for pair in conversion_str.split_whitespace() {
1979 let (additive, sides) = if pair.contains("->") {
1980 (true, pair.split("->").collect::<Vec<_>>())
1981 } else if pair.contains("<-") {
1982 (false, pair.split("<-").collect::<Vec<_>>())
1983 } else {
1984 continue;
1985 };
1986
1987 if sides.len() != 2 {
1988 continue;
1989 }
1990
1991 let source_spec = sides[0];
1992 let target = sides[1].to_string();
1993
1994 let source_colors: Vec<String> = if source_spec == "AnyColor" {
1995 vec!["W", "U", "B", "R", "G"]
1996 .into_iter()
1997 .map(String::from)
1998 .collect()
1999 } else if source_spec == "AnyType" {
2000 vec!["W", "U", "B", "R", "G", "C"]
2001 .into_iter()
2002 .map(String::from)
2003 .collect()
2004 } else if let Some(excluded) = source_spec.strip_prefix("non") {
2005 vec!["W", "U", "B", "R", "G"]
2006 .into_iter()
2007 .filter(|c| !c.eq_ignore_ascii_case(excluded))
2008 .map(String::from)
2009 .collect()
2010 } else {
2011 vec![source_spec.to_string()]
2012 };
2013
2014 for source in source_colors {
2015 if additive {
2016 conversions.entry(source).or_default().push(target.clone());
2017 } else {
2018 conversions.insert(source, vec![target.clone()]);
2020 }
2021 }
2022 }
2023}
2024
2025pub fn apply_text_change_effects(
2033 def: &str,
2034 is_descriptive: bool,
2035 color_map: &std::collections::BTreeMap<String, String>,
2036 type_map: &std::collections::BTreeMap<String, String>,
2037) -> String {
2038 if def.is_empty() {
2039 return def.to_string();
2040 }
2041
2042 let mut replaced = def.to_string();
2043
2044 for (key, value) in color_map {
2046 if key == "Any" {
2047 for color in &["white", "blue", "black", "red", "green"] {
2048 let cap = capitalize(color);
2049 if value.eq_ignore_ascii_case(color) {
2050 continue; }
2052 replaced = replaced.replace(color, &value.to_lowercase());
2053 replaced = replaced.replace(&cap, &capitalize(value));
2054 }
2055 } else {
2056 replaced = replaced.replace(&key.to_lowercase(), &value.to_lowercase());
2057 replaced = replaced.replace(key, value);
2058 }
2059 }
2060
2061 for (key, value) in type_map {
2063 if is_descriptive {
2064 let plural_key = pluralize_type(key);
2066 let plural_value = pluralize_type(value);
2067 replaced = replaced.replace(&plural_key, &plural_value);
2068 }
2069 replaced = replaced.replace(key, value);
2070 }
2071
2072 replaced
2073}
2074
2075fn extract_text_change_maps(
2078 card: &Card,
2079) -> (
2080 std::collections::BTreeMap<String, String>,
2081 std::collections::BTreeMap<String, String>,
2082) {
2083 let mut color_map = std::collections::BTreeMap::new();
2084 let mut type_map = std::collections::BTreeMap::new();
2085 for (key, value) in &card.svars {
2086 if let Some(from) = key.strip_prefix("TextColor:") {
2087 color_map.insert(from.to_string(), value.clone());
2088 } else if let Some(from) = key.strip_prefix("TextType:") {
2089 type_map.insert(from.to_string(), value.clone());
2090 }
2091 }
2092 (color_map, type_map)
2093}
2094
2095pub fn apply_ability_text_change_effects(def: &str, card: &Card) -> String {
2098 let (color_map, type_map) = extract_text_change_maps(card);
2099 apply_text_change_effects(def, false, &color_map, &type_map)
2100}
2101
2102pub fn apply_keyword_text_change_effects(kw: &str, card: &Card) -> String {
2105 let (color_map, type_map) = extract_text_change_maps(card);
2106 apply_text_change_effects(kw, false, &color_map, &type_map)
2107}
2108
2109pub fn apply_description_text_change_effects(def: &str, card: &Card) -> String {
2113 let (color_map, type_map) = extract_text_change_maps(card);
2114 apply_text_change_effects(def, true, &color_map, &type_map)
2115}
2116
2117fn capitalize(s: &str) -> String {
2118 let mut chars = s.chars();
2119 match chars.next() {
2120 None => String::new(),
2121 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
2122 }
2123}
2124
2125fn pluralize_type(type_name: &str) -> String {
2126 if type_name.ends_with('s') {
2127 type_name.to_string()
2128 } else if let Some(stem) = type_name.strip_suffix('y') {
2129 format!("{stem}ies")
2130 } else {
2131 format!("{type_name}s")
2132 }
2133}
2134
2135pub fn add_splice_effects(
2146 game: &GameState,
2147 sa: SpellAbility,
2148 _agents: &mut [Box<dyn crate::agent::PlayerAgent>],
2149) -> SpellAbility {
2150 let source_id = match sa.source {
2151 Some(id) => id,
2152 None => return sa,
2153 };
2154 let source = game.card(source_id);
2155 let player = sa.activating_player;
2156
2157 if !sa.is_spell || sa.is_copy {
2159 return sa;
2160 }
2161
2162 let hand = game.cards_in_zone(ZoneType::Hand, player);
2163 if hand.is_empty() {
2164 return sa;
2165 }
2166
2167 let splice_candidates: Vec<CardId> = hand
2169 .iter()
2170 .copied()
2171 .filter(|&cid| {
2172 if cid == source_id {
2173 return false;
2174 }
2175 let card = game.card(cid);
2176 if let Some(splice_rest) = card.keywords.find_with_prefix("Splice onto ") {
2178 let splice_type = splice_rest
2179 .strip_prefix("Splice onto ")
2180 .unwrap_or("")
2181 .split(' ')
2182 .next()
2183 .unwrap_or("");
2184 source
2185 .type_line
2186 .core_types
2187 .iter()
2188 .any(|ct| ct.name().eq_ignore_ascii_case(splice_type))
2189 || splice_type.eq_ignore_ascii_case("instant") && source.type_line.is_instant()
2190 || splice_type.eq_ignore_ascii_case("arcane")
2191 && source
2192 .type_line
2193 .subtypes
2194 .iter()
2195 .any(|s| s.eq_ignore_ascii_case("Arcane"))
2196 } else {
2197 false
2198 }
2199 })
2200 .collect();
2201
2202 if splice_candidates.is_empty() {
2203 return sa;
2204 }
2205
2206 sa
2210}
2211
2212pub fn add_splice_effect(sa: &mut SpellAbility, game: &GameState, splice_card_id: CardId) {
2218 let splice_card = game.card(splice_card_id);
2219
2220 let first_ability = match splice_card.abilities.first() {
2222 Some(text) => text.clone(),
2223 None => return,
2224 };
2225
2226 let mut sub_sa = crate::spellability::build_spell_ability(
2228 game,
2229 splice_card_id,
2230 &first_ability,
2231 sa.activating_player,
2232 );
2233 sub_sa.source = sa.source; sub_sa.activating_player = sa.activating_player;
2235
2236 let mut slot = &mut sa.sub_ability;
2238 loop {
2239 match slot {
2240 Some(node) => slot = &mut node.sub_ability,
2241 None => {
2242 *slot = Some(Box::new(sub_sa));
2243 break;
2244 }
2245 }
2246 }
2247
2248 sa.spliced_cards.push(splice_card_id);
2250
2251 let name = splice_card.card_name.clone();
2253 if !sa.description.is_empty() {
2254 sa.description
2255 .push_str(&format!(" (Splicing {name} onto it)"));
2256 }
2257}
2258
2259#[allow(clippy::type_complexity)]
2269pub fn get_defined_objects(
2270 defined: &str,
2271 sa: &SpellAbility,
2272 game: &GameState,
2273) -> (Vec<PlayerId>, Vec<CardId>, Vec<SpellAbility>) {
2274 let d = if defined.is_empty() { "Self" } else { defined };
2275 (
2276 resolve_defined_players_with_sa(d, sa, sa.activating_player, game),
2277 get_defined_cards(game, sa.source, d, Some(sa.activating_player)),
2278 get_defined_spell_abilities(d, sa, game),
2279 )
2280}
2281
2282pub fn get_defined_entities(
2285 defined: &str,
2286 sa: &SpellAbility,
2287 game: &GameState,
2288) -> (Vec<PlayerId>, Vec<CardId>) {
2289 let d = if defined.is_empty() { "Self" } else { defined };
2290 (
2291 resolve_defined_players_with_sa(d, sa, sa.activating_player, game),
2292 get_defined_cards(game, sa.source, d, Some(sa.activating_player)),
2293 )
2294}
2295
2296pub fn get_basic_spells_from_play_effect(
2304 game: &GameState,
2305 tgt_card: CardId,
2306 controller: PlayerId,
2307) -> Vec<SpellAbility> {
2308 get_spells_from_play_effect(game, tgt_card, controller, false)
2309}
2310
2311pub fn get_spells_from_play_effect(
2319 game: &GameState,
2320 tgt_card: CardId,
2321 controller: PlayerId,
2322 _with_alt_cost: bool,
2323) -> Vec<SpellAbility> {
2324 let card = game.card(tgt_card);
2325 let mut out = Vec::new();
2326 for ab_text in &card.abilities {
2327 let params = crate::parsing::Params::from_raw(ab_text);
2328 let record = crate::ability::ability_factory::AbilityRecordType::from_params(¶ms);
2329 let is_spell = matches!(
2330 record,
2331 Some(crate::ability::ability_factory::AbilityRecordType::Spell)
2332 );
2333 if !is_spell && !card.type_line.is_land() {
2334 continue;
2335 }
2336 let built =
2337 crate::spellability::build_spell_ability_from_host_card(card, ab_text, controller);
2338 out.push(built);
2339 }
2340 out
2341}
2342
2343pub fn get_s_var(sa: &SpellAbility, game: &GameState, svar_name: &str) -> Option<String> {
2346 let host_id = sa.source?;
2347 let raw = game.card(host_id).get_s_var(svar_name)?.to_string();
2348 if !sa.is_intrinsic() || raw.is_empty() {
2349 return Some(raw);
2350 }
2351 Some(raw)
2356}
2357
2358pub fn is_unlinked_from_cast_sa(sa: &SpellAbility, card: &Card) -> bool {
2364 if !sa.is_intrinsic() {
2365 return false;
2366 }
2367 if sa.source != Some(card.id) {
2368 return false;
2369 }
2370 let trait_host = sa.original_host.or(sa.source);
2375 let Some(cast) = card.cast_sa.as_deref() else {
2376 return false;
2377 };
2378 let cast_host = cast.original_host.or(cast.source);
2379 match (trait_host, cast_host) {
2380 (Some(t), Some(c)) => t != c,
2381 _ => false,
2382 }
2383}
2384
2385pub fn resolve(ctx: &mut crate::ability::effects::EffectContext, sa: &SpellAbility) {
2394 crate::ability::effects::resolve_effect(ctx, sa);
2395}