1use forge_foundation::mana::ManaAtom;
2use forge_foundation::ZoneType;
3
4use crate::ability::{ProducedMana, ProducedManaCombo};
5use crate::agent::PlayerAgent;
6use crate::card::Card;
7use crate::cost::CostPart;
8use crate::game::GameState;
9use crate::ids::{CardId, PlayerId};
10use crate::spellability::SpellAbility;
11
12pub mod auto_pay;
13pub mod computer_util_mana;
14pub mod mana_conversion_matrix;
15pub mod mana_cost_being_paid;
16pub mod mana_pool;
17pub mod mana_refund_service;
18pub use auto_pay::{
19 pay_mana_cost_auto, pay_mana_cost_auto_with_callback,
20 pay_mana_cost_auto_with_callback_and_reserved_sacrifices, pay_mana_cost_auto_with_chooser,
21 AutoPayResult,
22};
23
24pub fn apply_player_life_payment_keywords(
25 game: &GameState,
26 player: PlayerId,
27 cost: &forge_foundation::ManaCost,
28) -> forge_foundation::ManaCost {
29 let mut result = cost.clone();
30 if crate::player::has_keyword(game, player, "PayLifeInsteadOf:W") {
31 result = result.colored_to_phyrexian(ManaAtom::WHITE as u8);
32 }
33 if crate::player::has_keyword(game, player, "PayLifeInsteadOf:U") {
34 result = result.colored_to_phyrexian(ManaAtom::BLUE as u8);
35 }
36 if crate::player::has_keyword(game, player, "PayLifeInsteadOf:B") {
37 result = result.colored_to_phyrexian(ManaAtom::BLACK as u8);
38 }
39 if crate::player::has_keyword(game, player, "PayLifeInsteadOf:R") {
40 result = result.colored_to_phyrexian(ManaAtom::RED as u8);
41 }
42 if crate::player::has_keyword(game, player, "PayLifeInsteadOf:G") {
43 result = result.colored_to_phyrexian(ManaAtom::GREEN as u8);
44 }
45 result
46}
47
48pub(crate) fn mana_ability_meets_script_requirements(
49 game: &GameState,
50 card_id: CardId,
51 ab: &crate::ability::activated::ActivatedAbility,
52) -> bool {
53 let card = game.card(card_id);
54 let requirements = crate::card::valid_filter::CardTraitRequirementsIr::from_key_values(
55 ab.params
56 .inner()
57 .iter()
58 .map(|(key, value)| (key.as_str(), value.as_str())),
59 None,
60 None,
61 );
62 requirements.meets(game, card, card)
63}
64
65pub use computer_util_mana::{
66 auto_tap_lands, auto_tap_lands_allow_reserved_source_reuse,
67 auto_tap_lands_allow_reserved_source_reuse_trace,
68 auto_tap_lands_allow_reserved_source_reuse_trace_with_callbacks_and_reserved_sacrifices,
69 auto_tap_lands_allow_reserved_source_reuse_with_callbacks,
70 auto_tap_lands_allow_reserved_source_reuse_with_callbacks_and_reserved_sacrifices,
71 auto_tap_lands_allow_reserved_source_reuse_with_chooser, auto_tap_lands_generic,
72 auto_tap_lands_trace, auto_tap_lands_trace_with_callbacks, auto_tap_lands_with_callbacks,
73 auto_tap_lands_with_chooser, can_pay_mana_cost_with_reserved_sacrifices,
74 can_pay_spell_mana_cost_for_action_space, collect_mana_payment_sources, next_auto_tap_choice,
75 next_auto_tap_choice_with_reserved_sacrifices, AutoTapChoice, ManaPayCallback,
76 ManaPayCallbackFn, ManaPaymentSources, SacrificeChooser,
77};
78
79impl ProducedMana {
80 pub fn to_atoms(&self, chosen_colors: &[String]) -> Vec<u16> {
81 let mut atoms = Vec::new();
82 match self {
83 Self::Any => add_any_colors(&mut atoms),
84 Self::Chosen => return chosen_colors_to_atoms(chosen_colors),
85 Self::Combo(combo) => match combo {
86 ProducedManaCombo::Any => add_any_colors(&mut atoms),
87 ProducedManaCombo::Chosen => {
88 for atom in chosen_colors_to_atoms(chosen_colors) {
89 unique_push(&mut atoms, atom);
90 }
91 }
92 ProducedManaCombo::ColorIdentity => {}
93 ProducedManaCombo::Colors(colors) => {
94 for color in colors {
95 if let Some(atom) = mana_atom_from_produced(color) {
96 unique_push(&mut atoms, atom);
97 }
98 }
99 }
100 ProducedManaCombo::Raw(raw) => {
101 for part in raw.split_whitespace() {
102 if part.eq_ignore_ascii_case("Any") {
103 add_any_colors(&mut atoms);
104 } else if part.eq_ignore_ascii_case("Chosen") {
105 for atom in chosen_colors_to_atoms(chosen_colors) {
106 unique_push(&mut atoms, atom);
107 }
108 } else if let Some(atom) = mana_atom_from_produced(part) {
109 unique_push(&mut atoms, atom);
110 }
111 }
112 }
113 },
114 Self::Special(_) => {}
115 Self::Fixed(tokens) => {
116 for part in tokens.iter().flat_map(|token| token.split_whitespace()) {
117 if let Some(atom) = mana_atom_from_produced(part) {
118 unique_push(&mut atoms, atom);
119 }
120 }
121 }
122 Self::Raw(raw) => {
123 for part in raw.split_whitespace() {
124 if let Some(atom) = mana_atom_from_produced(part) {
125 unique_push(&mut atoms, atom);
126 }
127 }
128 }
129 }
130 atoms
131 }
132
133 pub fn fixed_atoms(&self) -> Option<Vec<u16>> {
134 let tokens = self.fixed_tokens()?;
135 if tokens.len() <= 1 {
136 return None;
137 }
138
139 let mut atoms = Vec::with_capacity(tokens.len());
140 for part in tokens {
141 atoms.push(mana_atom_from_produced(part)?);
142 }
143 Some(atoms)
144 }
145
146 pub fn to_color_names(&self, chosen_colors: &[String]) -> Vec<String> {
147 let mut colors = Vec::new();
148 for atom in self.to_atoms(chosen_colors) {
149 if let Some(name) = mana_atom_to_color_name(atom) {
150 colors.push(name.to_string());
151 }
152 }
153 colors
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct Mana {
160 pub color: u16,
161 pub source_card: Option<CardId>,
162 pub is_snow: bool,
163 pub is_persistent: bool,
165 pub is_combat_mana: bool,
167 pub restriction: Option<String>,
170 pub adds_no_counter: bool,
172 pub adds_keywords: Option<String>,
175 pub adds_keywords_valid: Option<String>,
177 pub adds_counters: Option<String>,
179 pub adds_counters_valid: Option<String>,
181 pub triggers_when_spent: Option<String>,
184}
185
186impl Mana {
187 pub fn simple(color: u16) -> Self {
188 Self {
189 color,
190 source_card: None,
191 is_snow: false,
192 is_persistent: false,
193 is_combat_mana: false,
194 restriction: None,
195 adds_no_counter: false,
196 adds_keywords: None,
197 adds_keywords_valid: None,
198 adds_counters: None,
199 adds_counters_valid: None,
200 triggers_when_spent: None,
201 }
202 }
203
204 pub fn adds_no_counter_magic(&self) -> bool {
207 self.adds_no_counter
208 }
209
210 pub fn adds_counters(&self) -> bool {
213 self.adds_counters.is_some()
214 }
215
216 pub fn adds_keywords(&self) -> bool {
219 self.adds_keywords.is_some()
220 }
221
222 pub fn adds_keywords_type(&self) -> bool {
225 self.adds_keywords_valid.is_some()
226 }
227
228 pub fn adds_keywords_until(&self) -> bool {
231 self.adds_keywords.is_some()
233 }
234
235 pub fn triggers_when_spent(&self) -> bool {
238 self.triggers_when_spent.is_some()
239 }
240}
241
242#[derive(Debug, Clone, Default)]
244pub struct ManaPaymentContext {
245 pub is_spell: bool,
247 pub is_activated_ability: bool,
251 pub sa_on_stack: bool,
262 pub type_line: Option<forge_foundation::CardTypeLine>,
265 pub card_name: Option<String>,
267 pub card_color: Option<forge_foundation::ColorSet>,
269 pub chosen_types_by_source: std::collections::HashMap<CardId, String>,
271}
272
273pub fn payment_context_for_sa(game: &GameState, sa: &SpellAbility) -> ManaPaymentContext {
274 let (type_line, card_name, card_color) = if let Some(source) = sa.source {
275 let card = game.card(source);
276 (
277 Some(card.type_line.clone()),
278 Some(card.card_name.clone()),
279 Some(card.color),
280 )
281 } else {
282 (None, None, None)
283 };
284
285 ManaPaymentContext {
286 is_spell: sa.is_spell,
287 is_activated_ability: sa.is_activated,
288 sa_on_stack: false,
293 type_line,
294 card_name,
295 card_color,
296 chosen_types_by_source: game
297 .cards
298 .iter()
299 .filter_map(|c| c.chosen_type.clone().map(|chosen| (c.id, chosen)))
300 .collect(),
301 }
302}
303
304pub fn mana_meets_restriction(restriction: &str, ctx: &ManaPaymentContext) -> bool {
306 for part in restriction.split(',') {
308 let part = part.trim();
309 if part.is_empty() {
310 continue;
311 }
312 if check_single_restriction(part, ctx) {
313 return true;
314 }
315 }
316 false
317}
318
319fn check_single_restriction(restriction: &str, ctx: &ManaPaymentContext) -> bool {
320 match restriction {
321 "nonSpell" => !ctx.is_spell,
322 "Activated" => ctx.is_activated_ability,
323 "Spell" => ctx.is_spell,
324 _ if restriction.starts_with("Spell.") => {
325 if !ctx.is_spell {
326 return false;
327 }
328 let type_check = &restriction[6..]; if let Some(ref tl) = ctx.type_line {
330 match type_check {
331 "Creature" => tl.is_creature(),
332 "Artifact" => tl.is_artifact(),
333 "Enchantment" => tl.is_enchantment(),
334 "Instant" => tl.is_instant(),
335 "Sorcery" => tl.is_sorcery(),
336 "Planeswalker" => tl.is_planeswalker(),
337 "Land" => tl.is_land(),
338 other => {
339 if let Some((base, sub)) = other.split_once('+') {
342 let base_ok = match base {
343 "Creature" => tl.is_creature(),
344 "Artifact" => tl.is_artifact(),
345 _ => tl.has_subtype(base),
346 };
347 let sub_ok = match sub {
348 "Colorless" => ctx.card_color.is_some_and(|c| c.is_colorless()),
349 "Multicolor" => ctx.card_color.is_some_and(|c| c.is_multicolor()),
350 _ => tl.has_subtype(sub),
351 };
352 base_ok && sub_ok
353 } else {
354 tl.has_subtype(other)
355 }
356 }
357 }
358 } else {
359 false
360 }
361 }
362 _ if restriction.starts_with("Activated.") => {
363 if !ctx.is_activated_ability {
368 return false;
369 }
370 let type_check = &restriction[10..]; let Some(tl) = ctx.type_line.as_ref() else {
372 return false;
373 };
374 match type_check {
375 "Creature" => tl.is_creature(),
376 "Artifact" => tl.is_artifact(),
377 "Enchantment" => tl.is_enchantment(),
378 "Land" => tl.is_land(),
379 "Planeswalker" => tl.is_planeswalker(),
380 other => {
381 if let Some((base, sub)) = other.split_once('+') {
382 let base_ok = match base {
383 "Creature" => tl.is_creature(),
384 "Artifact" => tl.is_artifact(),
385 _ => tl.has_subtype(base),
386 };
387 base_ok && tl.has_subtype(sub)
388 } else {
389 tl.has_subtype(other)
390 }
391 }
392 }
393 }
394 _ if restriction.starts_with("CantPayGenericCosts") => true, _ if restriction.starts_with("CantCast") => true, _ => true, }
398}
399
400pub use mana_pool::ManaPool;
402
403pub fn basic_land_mana_atom(card: &Card) -> Option<u16> {
407 if card.type_line.has_subtype("Plains") {
408 Some(ManaAtom::WHITE)
409 } else if card.type_line.has_subtype("Island") {
410 Some(ManaAtom::BLUE)
411 } else if card.type_line.has_subtype("Swamp") {
412 Some(ManaAtom::BLACK)
413 } else if card.type_line.has_subtype("Mountain") {
414 Some(ManaAtom::RED)
415 } else if card.type_line.has_subtype("Forest") {
416 Some(ManaAtom::GREEN)
417 } else {
418 match card.card_name.as_str() {
420 "Plains" => Some(ManaAtom::WHITE),
421 "Island" => Some(ManaAtom::BLUE),
422 "Swamp" => Some(ManaAtom::BLACK),
423 "Mountain" => Some(ManaAtom::RED),
424 "Forest" => Some(ManaAtom::GREEN),
425 _ => None,
426 }
427 }
428}
429
430pub fn mana_atom_from_produced(produced: &str) -> Option<u16> {
432 match produced.trim() {
433 "W" => Some(ManaAtom::WHITE),
434 "U" => Some(ManaAtom::BLUE),
435 "B" => Some(ManaAtom::BLACK),
436 "R" => Some(ManaAtom::RED),
437 "G" => Some(ManaAtom::GREEN),
438 "C" => Some(ManaAtom::COLORLESS),
439 _ => None,
440 }
441}
442
443pub(crate) fn mana_atom_to_color_name(atom: u16) -> Option<&'static str> {
444 match atom {
445 ManaAtom::WHITE => Some("White"),
446 ManaAtom::BLUE => Some("Blue"),
447 ManaAtom::BLACK => Some("Black"),
448 ManaAtom::RED => Some("Red"),
449 ManaAtom::GREEN => Some("Green"),
450 ManaAtom::COLORLESS => Some("Colorless"),
451 _ => None,
452 }
453}
454
455fn unique_push(atoms: &mut Vec<u16>, atom: u16) {
456 if !atoms.contains(&atom) {
457 atoms.push(atom);
458 }
459}
460
461fn add_any_colors(atoms: &mut Vec<u16>) {
462 unique_push(atoms, ManaAtom::WHITE);
463 unique_push(atoms, ManaAtom::BLUE);
464 unique_push(atoms, ManaAtom::BLACK);
465 unique_push(atoms, ManaAtom::RED);
466 unique_push(atoms, ManaAtom::GREEN);
467}
468
469fn chosen_colors_to_atoms(chosen_colors: &[String]) -> Vec<u16> {
470 let mut atoms = Vec::new();
471 for chosen in chosen_colors {
472 if let Some(atom) = color_name_to_mana_atom(chosen) {
473 unique_push(&mut atoms, atom);
474 continue;
475 }
476 if let Some(atom) = mana_atom_from_produced(chosen) {
477 unique_push(&mut atoms, atom);
478 }
479 }
480 atoms
481}
482
483pub fn mana_letter_to_color_name(letter: &str) -> Option<String> {
485 match letter.trim() {
486 "W" => Some("White".to_string()),
487 "U" => Some("Blue".to_string()),
488 "B" => Some("Black".to_string()),
489 "R" => Some("Red".to_string()),
490 "G" => Some("Green".to_string()),
491 "C" => Some("Colorless".to_string()),
492 _ => None,
493 }
494}
495
496pub(crate) fn compute_reflected_atoms(
500 game: &GameState,
501 player: PlayerId,
502 card_id: CardId,
503 ab: &crate::ability::activated::ActivatedAbility,
504) -> Vec<u16> {
505 let sa = crate::ability::ability_factory::build_spell_ability(
506 game,
507 card_id,
508 &ab.ability_text,
509 player,
510 );
511 let colors = crate::card::card_util::get_reflectable_mana_colors(game, &sa);
512 let mut reflected_atoms = Vec::new();
516 for (name, atom) in [
517 ("white", ManaAtom::WHITE),
518 ("blue", ManaAtom::BLUE),
519 ("black", ManaAtom::BLACK),
520 ("red", ManaAtom::RED),
521 ("green", ManaAtom::GREEN),
522 ("colorless", ManaAtom::COLORLESS),
523 ] {
524 if colors.contains(name) || colors.contains(&capitalize_color(name)) {
525 reflected_atoms.push(atom);
526 }
527 }
528 reflected_atoms
529}
530
531pub fn color_name_to_mana_atom(name: &str) -> Option<u16> {
534 match name.to_ascii_lowercase().as_str() {
535 "white" => Some(ManaAtom::WHITE),
536 "blue" => Some(ManaAtom::BLUE),
537 "black" => Some(ManaAtom::BLACK),
538 "red" => Some(ManaAtom::RED),
539 "green" => Some(ManaAtom::GREEN),
540 "colorless" => Some(ManaAtom::COLORLESS),
541 _ => None,
542 }
543}
544
545pub fn capitalize_color(s: &str) -> String {
547 let mut chars = s.chars();
548 match chars.next() {
549 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
550 None => String::new(),
551 }
552}
553
554pub(crate) fn all_basic_subtype_atoms(card: &Card) -> Vec<u16> {
558 let mut atoms = Vec::new();
559 let subtypes = [
560 ("Plains", ManaAtom::WHITE),
561 ("Island", ManaAtom::BLUE),
562 ("Swamp", ManaAtom::BLACK),
563 ("Mountain", ManaAtom::RED),
564 ("Forest", ManaAtom::GREEN),
565 ];
566 for (subtype, atom) in &subtypes {
567 if card.type_line.has_subtype(subtype) && !atoms.contains(atom) {
568 atoms.push(*atom);
569 }
570 }
571 atoms
572}
573
574pub(crate) fn tap_land_for_mana(
575 game: &mut GameState,
576 pool: &mut ManaPool,
577 player: PlayerId,
578 land_id: CardId,
579 atom: u16,
580 should_tap: bool,
581 tapped_lands: &mut Vec<CardId>,
582 ability_index: Option<usize>,
583) {
584 let _ = player;
585 let card = game.card(land_id);
586 let is_snow = card.type_line.is_snow();
587 let triggers_when_spent = ability_index
593 .and_then(|idx| card.activated_abilities.get(idx))
594 .and_then(|ab| ab.triggers_when_spent.clone());
595 if should_tap && !card.tapped {
596 game.tap(land_id);
597 }
598 let mut mana = if is_snow {
599 let mut m = crate::mana::Mana::simple(atom);
600 m.is_snow = true;
601 m
602 } else {
603 crate::mana::Mana::simple(atom)
604 };
605 mana.source_card = Some(land_id);
606 mana.triggers_when_spent = triggers_when_spent;
607 if std::env::var("FORGE_PAYMENT_TRACE").is_ok() {
608 let card_name = game.card(land_id).card_name.clone();
609 let turn = game.turn.turn_number;
610 let phase = format!("{:?}", game.turn.phase);
611 eprintln!(
612 "[pay-trace-rust] T{} {} P{:?} tap_land_for_mana card={}#{:?} atom={} pool_total={} ability_idx={:?}",
613 turn,
614 phase,
615 player,
616 card_name,
617 land_id,
618 ManaPool::atom_to_letter(atom),
619 pool.total_mana(),
620 ability_index,
621 );
622 }
623 pool.add_mana(mana);
624 tapped_lands.push(land_id);
625}
626
627pub fn land_mana_atoms(card: &Card) -> Vec<u16> {
635 let mut atoms = Vec::new();
636 for ab in &card.activated_abilities {
637 if !ab.is_mana_ability {
638 continue;
639 }
640 if ab
643 .cost
644 .parts
645 .iter()
646 .any(|p| matches!(p, CostPart::Mana { .. }))
647 {
648 continue;
649 }
650 if let Some(produced_ir) = ab.produced_ir.as_ref() {
651 if produced_ir.is_combo_color_identity() {
652 } else {
657 for atom in produced_ir.to_atoms(&card.chosen_colors) {
658 if !atoms.contains(&atom) {
659 atoms.push(atom);
660 }
661 }
662 }
663 }
664 }
665 if atoms.is_empty() {
670 atoms = all_basic_subtype_atoms(card);
671 if atoms.is_empty() {
673 if let Some(a) = basic_land_mana_atom(card) {
674 atoms.push(a);
675 }
676 }
677 }
678 atoms
679}
680
681pub(crate) fn atom_short(atom: u16) -> &'static str {
682 match atom {
683 ManaAtom::WHITE => "W",
684 ManaAtom::BLUE => "U",
685 ManaAtom::BLACK => "B",
686 ManaAtom::RED => "R",
687 ManaAtom::GREEN => "G",
688 ManaAtom::COLORLESS => "C",
689 _ => "1",
690 }
691}
692
693pub struct ManaProductionParams {
697 pub source_card: CardId,
698 pub is_snow: bool,
699 pub restriction: Option<String>,
700 pub adds_no_counter: bool,
701 pub adds_keywords: Option<String>,
702 pub adds_keywords_valid: Option<String>,
703 pub adds_counters: Option<String>,
704 pub adds_counters_valid: Option<String>,
705 pub triggers_when_spent: Option<String>,
706}
707
708pub fn determine_mana_production_ir(
717 game: &mut GameState,
718 agents: &mut [Box<dyn PlayerAgent>],
719 player: PlayerId,
720 card_id: CardId,
721 produced_ir: &ProducedMana,
722 produced_text: &str,
723 amount_param: Option<&str>,
724 express_choice: Option<u16>,
725) -> Option<String> {
726 let mut mana_string: Option<String> = None;
727 let amount = amount_param.map(|amount_str| {
728 if let Ok(n) = amount_str.parse::<i32>() {
729 n
730 } else if let Some(svar_expr) = game.card(card_id).svars.get(amount_str).cloned() {
731 crate::ability::effects::resolve_count_svar(&svar_expr, game, card_id, player)
732 } else {
733 1
734 }
735 });
736 let uses_combo_distribution = amount.is_some_and(|amount| amount > 1)
737 && matches!(produced_ir, ProducedMana::Combo(_))
738 && !produced_ir.is_combo_color_identity();
739
740 if uses_combo_distribution {
742 let available: Vec<String> = if produced_ir.is_any_like() {
743 vec!["W", "U", "B", "R", "G"]
744 .into_iter()
745 .map(String::from)
746 .collect()
747 } else {
748 let chosen_colors = game.card(card_id).chosen_colors.clone();
749 let names = produced_ir.to_color_names(&chosen_colors);
750 names
751 .iter()
752 .filter_map(|name| {
753 color_name_to_mana_atom(name).map(|a| ManaPool::atom_to_letter(a).to_string())
754 })
755 .collect()
756 };
757 if !available.is_empty() {
758 let chosen = agents[player.index()].specify_mana_combo(
759 player,
760 &available,
761 amount.unwrap_or(1) as usize,
762 Some(card_id),
763 express_choice,
764 );
765 mana_string = Some(chosen.join(" "));
766 }
767 } else if produced_ir.is_combo_color_identity() {
768 let colors = game.player_commander_color_identity(player);
769
770 if !colors.is_empty() {
771 if let Some(chosen) = agents[player.index()].choose_color(player, &colors) {
772 if let Some(atom) = color_name_to_mana_atom(&chosen) {
773 mana_string = Some(ManaPool::atom_to_letter(atom).to_string());
774 }
775 }
776 }
777 } else if produced_ir.fixed_atoms().is_some() {
778 mana_string = Some(produced_text.to_string());
779 } else {
780 let chosen_colors = game.card(card_id).chosen_colors.clone();
781 let colors = produced_ir.to_color_names(&chosen_colors);
782 if colors.len() > 1 {
783 let chosen = if let Some(forced) = express_choice
784 .and_then(mana_atom_to_color_name)
785 .and_then(|forced_name| {
786 colors
787 .iter()
788 .find(|valid| valid.eq_ignore_ascii_case(forced_name))
789 .cloned()
790 }) {
791 let single = vec![forced.clone()];
795 let _ = agents[player.index()].choose_color(player, &single);
796 Some(forced)
797 } else {
798 agents[player.index()].choose_color(player, &colors)
799 };
800 if let Some(chosen) = chosen {
801 if let Some(atom) = color_name_to_mana_atom(&chosen) {
802 mana_string = Some(ManaPool::atom_to_letter(atom).to_string());
803 }
804 }
805 } else if let Some(single) = colors.first() {
806 if let Some(atom) = color_name_to_mana_atom(single) {
807 mana_string = Some(ManaPool::atom_to_letter(atom).to_string());
808 }
809 } else {
810 mana_string = Some(produced_text.to_string());
812 }
813 }
814
815 if let Some(ref mut ms) = mana_string {
817 if let Some(amount) = amount {
818 if amount > 1 {
819 if !uses_combo_distribution {
820 let base = ms.clone();
821 for _ in 1..amount {
822 ms.push(' ');
823 ms.push_str(&base);
824 }
825 }
826 } else if amount <= 0 {
827 mana_string = None;
828 }
829 }
830 }
831
832 if let Some(ref mut ms) = mana_string {
834 use crate::replacement::replacement_handler::{
835 apply_replacements_with_agents, ReplacementEvent,
836 };
837 let mut event = ReplacementEvent::ProduceMana {
838 source: card_id,
839 activator: player,
840 mana: ms.clone(),
841 };
842 let result = apply_replacements_with_agents(game, agents, &mut event);
843 if result == crate::replacement::ReplacementResult::Updated {
844 if let ReplacementEvent::ProduceMana { mana: new_mana, .. } = event {
845 *ms = new_mana;
846 }
847 }
848 }
849
850 mana_string
851}
852
853pub fn add_produced_mana_to_pool(
855 pool: &mut ManaPool,
856 mana_string: &str,
857 params: &ManaProductionParams,
858) {
859 pool.produce_mana_from_string(
860 mana_string,
861 Some(params.source_card),
862 params.is_snow,
863 params.restriction.clone(),
864 params.adds_no_counter,
865 params.adds_keywords.clone(),
866 params.adds_keywords_valid.clone(),
867 params.adds_counters.clone(),
868 params.adds_counters_valid.clone(),
869 params.triggers_when_spent.clone(),
870 );
871}
872
873pub fn calculate_available_mana(pool: &ManaPool, game: &GameState, player: PlayerId) -> ManaPool {
881 calculate_available_mana_excluding_with_reserved(pool, game, player, None, &[])
882}
883
884pub fn calculate_available_mana_for_casting(
885 pool: &ManaPool,
886 game: &GameState,
887 player: PlayerId,
888) -> ManaPool {
889 calculate_available_mana_for_casting_excluding(pool, game, player, None)
890}
891
892pub fn calculate_available_mana_for_casting_excluding(
893 pool: &ManaPool,
894 game: &GameState,
895 player: PlayerId,
896 excluded_source: Option<CardId>,
897) -> ManaPool {
898 calculate_available_mana_excluding_with_reserved_impl(
899 pool,
900 game,
901 player,
902 excluded_source,
903 &[],
904 true,
905 None,
906 )
907}
908
909pub fn calculate_available_mana_excluding(
915 pool: &ManaPool,
916 game: &GameState,
917 player: PlayerId,
918 excluded_source: Option<CardId>,
919) -> ManaPool {
920 calculate_available_mana_excluding_with_reserved(pool, game, player, excluded_source, &[])
921}
922
923pub fn calculate_available_mana_excluding_with_reserved(
924 pool: &ManaPool,
925 game: &GameState,
926 player: PlayerId,
927 excluded_source: Option<CardId>,
928 reserved_sacrifices: &[CardId],
929) -> ManaPool {
930 calculate_available_mana_excluding_with_reserved_impl(
931 pool,
932 game,
933 player,
934 excluded_source,
935 reserved_sacrifices,
936 false,
937 None,
938 )
939}
940
941pub fn calculate_available_mana_with_context(
947 pool: &ManaPool,
948 game: &GameState,
949 player: PlayerId,
950 excluded_source: Option<CardId>,
951 reserved_sacrifices: &[CardId],
952 payment_ctx: Option<&ManaPaymentContext>,
953) -> ManaPool {
954 calculate_available_mana_excluding_with_reserved_impl(
958 pool,
959 game,
960 player,
961 excluded_source,
962 reserved_sacrifices,
963 true,
964 payment_ctx,
965 )
966}
967
968pub(crate) fn replacement_adjusted_atoms_for_availability(
969 game: &GameState,
970 player: PlayerId,
971 source: CardId,
972 atom: u16,
973) -> Vec<u16> {
974 use crate::replacement::replacement_handler::ReplacementEvent;
975
976 let mut event = ReplacementEvent::ProduceMana {
977 source,
978 activator: player,
979 mana: ManaPool::atom_to_letter(atom).to_string(),
980 };
981 if apply_produce_mana_replacements_for_availability(game, &mut event) {
982 let ReplacementEvent::ProduceMana { mana, .. } = event else {
983 return vec![atom];
984 };
985 let produced_ir = ProducedMana::from_raw_boundary(&mana);
986 let adjusted = produced_ir
987 .fixed_atoms()
988 .unwrap_or_else(|| produced_ir.to_atoms(&[]));
989 if !adjusted.is_empty() {
990 return adjusted;
991 }
992 }
993
994 vec![atom]
995}
996
997pub(crate) fn has_replacement_adjusted_available_mana(game: &GameState, player: PlayerId) -> bool {
998 fn is_adjusted(game: &GameState, player: PlayerId, source: CardId, atom: u16) -> bool {
999 let adjusted = replacement_adjusted_atoms_for_availability(game, player, source, atom);
1000 adjusted.len() != 1 || adjusted.first().copied() != Some(atom)
1001 }
1002
1003 for &card_id in game.cards_in_zone(ZoneType::Battlefield, player) {
1004 let card = game.card(card_id);
1005 if card.phased_out {
1006 continue;
1007 }
1008
1009 let mut has_explicit_mana = false;
1010 for ab in &card.activated_abilities {
1011 if !ab.is_mana_ability
1012 || ab
1013 .cost
1014 .parts
1015 .iter()
1016 .any(|part| matches!(part, CostPart::Mana { .. }))
1017 || (card.tapped
1018 && ab
1019 .cost
1020 .parts
1021 .iter()
1022 .any(|part| matches!(part, CostPart::Tap)))
1023 || !crate::cost::can_pay_ignoring_mana(&ab.cost, game, card_id, player)
1024 || !mana_ability_meets_script_requirements(game, card_id, ab)
1025 {
1026 continue;
1027 }
1028 has_explicit_mana = true;
1029
1030 let atoms = if ab.is_mana_reflected {
1031 compute_reflected_atoms(game, player, card_id, ab)
1032 } else if let Some(produced_ir) = ab.produced_ir.as_ref() {
1033 if produced_ir.is_combo_color_identity() {
1034 chosen_colors_to_atoms(&game.player_commander_color_identity(player))
1035 } else if let Some(fixed_atoms) = produced_ir.fixed_atoms() {
1036 fixed_atoms
1037 } else {
1038 produced_ir.to_atoms(&card.chosen_colors)
1039 }
1040 } else {
1041 Vec::new()
1042 };
1043
1044 if atoms
1045 .into_iter()
1046 .any(|atom| is_adjusted(game, player, card_id, atom))
1047 {
1048 return true;
1049 }
1050 }
1051
1052 if !has_explicit_mana && card.is_land() && !card.tapped {
1053 let mut atoms = all_basic_subtype_atoms(card);
1054 if atoms.is_empty() {
1055 if let Some(atom) = basic_land_mana_atom(card) {
1056 atoms.push(atom);
1057 }
1058 }
1059 if atoms
1060 .into_iter()
1061 .any(|atom| is_adjusted(game, player, card_id, atom))
1062 {
1063 return true;
1064 }
1065 }
1066 }
1067
1068 false
1069}
1070
1071pub(crate) fn java_replacement_filtered_atoms_for_availability(
1072 game: &GameState,
1073 player: PlayerId,
1074 source: CardId,
1075 ab: &crate::ability::activated::ActivatedAbility,
1076 intrinsic_atoms: &[u16],
1077) -> Vec<u16> {
1078 if intrinsic_atoms.is_empty() {
1079 return Vec::new();
1080 }
1081
1082 let orig_produced = ab
1083 .produced_ir
1084 .as_ref()
1085 .map(ProducedMana::as_script_text)
1086 .unwrap_or_else(|| {
1087 if ab.is_mana_reflected {
1088 "1".into()
1089 } else {
1090 "".into()
1091 }
1092 });
1093 if orig_produced.is_empty() {
1094 return intrinsic_atoms.to_vec();
1095 }
1096
1097 let mut event = crate::replacement::replacement_handler::ReplacementEvent::ProduceMana {
1098 source,
1099 activator: player,
1100 mana: orig_produced.to_string(),
1101 };
1102 if !apply_produce_mana_replacements_for_availability(game, &mut event) {
1103 return intrinsic_atoms.to_vec();
1104 }
1105
1106 let crate::replacement::replacement_handler::ReplacementEvent::ProduceMana { mana, .. } = event
1107 else {
1108 return intrinsic_atoms.to_vec();
1109 };
1110 if orig_produced.trim() == "Any" {
1120 return intrinsic_atoms.to_vec();
1121 }
1122
1123 let pairs = [
1124 ("W", ManaAtom::WHITE),
1125 ("U", ManaAtom::BLUE),
1126 ("B", ManaAtom::BLACK),
1127 ("R", ManaAtom::RED),
1128 ("G", ManaAtom::GREEN),
1129 ("C", ManaAtom::COLORLESS),
1130 ];
1131 let mut filtered = Vec::new();
1132 for (letter, atom) in pairs {
1133 if mana.contains(letter) && intrinsic_atoms.contains(&atom) && !filtered.contains(&atom) {
1134 filtered.push(atom);
1135 }
1136 }
1137 filtered
1138}
1139
1140pub(crate) fn reflected_atoms_for_availability(
1141 game: &GameState,
1142 player: PlayerId,
1143 source: CardId,
1144 ab: &crate::ability::activated::ActivatedAbility,
1145) -> Vec<u16> {
1146 let reflected = compute_reflected_atoms(game, player, source, ab);
1147 java_replacement_filtered_atoms_for_availability(game, player, source, ab, &reflected)
1148}
1149
1150fn apply_produce_mana_replacements_for_availability(
1151 game: &GameState,
1152 event: &mut crate::replacement::replacement_handler::ReplacementEvent,
1153) -> bool {
1154 use crate::replacement::{
1155 replace_produce_mana, ReplacementLayer, ReplacementResult, ReplacementType,
1156 };
1157
1158 let mut has_run: std::collections::HashSet<(CardId, usize)> = std::collections::HashSet::new();
1159 let mut updated = false;
1160
1161 loop {
1162 let mut changed_this_pass = false;
1163 for layer in [
1164 ReplacementLayer::CantHappen,
1165 ReplacementLayer::Control,
1166 ReplacementLayer::Copy,
1167 ReplacementLayer::Transform,
1168 ReplacementLayer::Other,
1169 ] {
1170 let mut chosen = None;
1171 'cards: for (i, card) in game.cards.iter().enumerate() {
1172 let card_id = CardId(i as u32);
1173 for (effect_idx, effect) in card.replacement_effects.iter().enumerate() {
1174 if has_run.contains(&(card_id, effect_idx))
1175 || effect.event != ReplacementType::ProduceMana
1176 || effect.layer != layer
1177 || !effect.active_in_zone(card.zone)
1178 || !effect.requirements_check(game, card)
1179 || !replace_produce_mana::can_replace(effect, event, game, card)
1180 {
1181 continue;
1182 }
1183 chosen = Some((card_id, effect_idx));
1184 break 'cards;
1185 }
1186 }
1187
1188 let Some((card_id, effect_idx)) = chosen else {
1189 continue;
1190 };
1191 has_run.insert((card_id, effect_idx));
1192 let effect = &game.card(card_id).replacement_effects[effect_idx];
1193 if replace_produce_mana::execute(effect, event, game, card_id)
1194 == ReplacementResult::Updated
1195 {
1196 updated = true;
1197 changed_this_pass = true;
1198 break;
1199 }
1200 }
1201
1202 if !changed_this_pass {
1203 return updated;
1204 }
1205 }
1206}
1207
1208fn atoms_mask_to_letters(mask: u16) -> String {
1209 let mut letters: Vec<&str> = Vec::new();
1210 for atom in [
1211 ManaAtom::WHITE,
1212 ManaAtom::BLUE,
1213 ManaAtom::BLACK,
1214 ManaAtom::RED,
1215 ManaAtom::GREEN,
1216 ManaAtom::COLORLESS,
1217 ] {
1218 if mask & atom != 0 {
1219 letters.push(ManaPool::atom_to_letter(atom));
1220 }
1221 }
1222 letters.join(" ")
1223}
1224
1225fn add_taps_for_mana_trigger_mana_for_availability(
1226 available: &mut ManaPool,
1227 source_count: &mut i32,
1228 source_colors: &mut Vec<u16>,
1229 game: &GameState,
1230 player: PlayerId,
1231 tapped_card_id: CardId,
1232 produced_letters: &str,
1233) {
1234 let params = crate::event::RunParams {
1235 card: Some(tapped_card_id),
1236 player: Some(player),
1237 activator: Some(player),
1238 produced: Some(produced_letters.to_string()),
1239 ..Default::default()
1240 };
1241
1242 for host in &game.cards {
1243 if host.zone != ZoneType::Battlefield || host.phased_out {
1244 continue;
1245 }
1246 for trigger in &host.triggers {
1247 if trigger.kind != crate::trigger::TriggerType::TapsForMana
1248 || !trigger.get_active_zone().contains(&host.zone)
1249 || !trigger.requirements_check(game, host.id)
1250 || !trigger.check_activation_limit(game, host.id)
1251 || !trigger.mode.perform_test(trigger, ¶ms, game)
1252 || !trigger.meets_requirements_on_triggered_objects(game, ¶ms, host.id)
1253 {
1254 continue;
1255 }
1256 let Some(svar_text) = host.svars.get(&trigger.execute) else {
1257 continue;
1258 };
1259 let trigger_params = crate::parsing::Params::from_raw(svar_text);
1260 if !trigger_params
1261 .get("DB")
1262 .is_some_and(|api| api.eq_ignore_ascii_case("Mana"))
1263 {
1264 continue;
1265 }
1266 let Some(produced) = trigger_params.get(crate::parsing::keys::PRODUCED) else {
1267 continue;
1268 };
1269 let amount = trigger_params
1270 .get(crate::parsing::keys::AMOUNT)
1271 .and_then(|raw| raw.parse::<i32>().ok())
1272 .unwrap_or(1)
1273 .max(1);
1274 let produced_ir = ProducedMana::from_raw_boundary(produced);
1275 if let Some(fixed_atoms) = produced_ir.fixed_atoms() {
1276 for atom in fixed_atoms {
1277 available.add(atom, 1);
1278 *source_count += 1;
1279 source_colors.push(atom);
1280 }
1281 continue;
1282 }
1283
1284 let atoms = produced_ir.to_atoms(&host.chosen_colors);
1285 if atoms.is_empty() {
1286 continue;
1287 }
1288 let src_mask = atoms.iter().fold(0, |mask, atom| mask | *atom);
1289 for atom in atoms {
1290 for _ in 0..amount {
1291 available.add(atom, 1);
1292 }
1293 }
1294 for _ in 0..amount {
1295 *source_count += 1;
1296 source_colors.push(src_mask);
1297 }
1298 }
1299 }
1300}
1301
1302fn calculate_available_mana_excluding_with_reserved_impl(
1303 pool: &ManaPool,
1304 game: &GameState,
1305 player: PlayerId,
1306 excluded_source: Option<CardId>,
1307 reserved_sacrifices: &[CardId],
1308 include_hand_sources: bool,
1309 payment_ctx: Option<&ManaPaymentContext>,
1310) -> ManaPool {
1311 let mut available = pool.clone();
1312 let battlefield = game.cards_in_zone(ZoneType::Battlefield, player);
1313 let hand_cards = if include_hand_sources {
1314 game.cards_in_zone(ZoneType::Hand, player).to_vec()
1315 } else {
1316 Vec::new()
1317 };
1318
1319 let mut source_count: i32 = 0;
1321
1322 let mut source_colors: Vec<u16> = Vec::new();
1325 for _ in 0..pool.white() {
1326 source_colors.push(ManaAtom::WHITE);
1327 }
1328 for _ in 0..pool.blue() {
1329 source_colors.push(ManaAtom::BLUE);
1330 }
1331 for _ in 0..pool.black() {
1332 source_colors.push(ManaAtom::BLACK);
1333 }
1334 for _ in 0..pool.red() {
1335 source_colors.push(ManaAtom::RED);
1336 }
1337 for _ in 0..pool.green() {
1338 source_colors.push(ManaAtom::GREEN);
1339 }
1340 source_colors.extend(std::iter::repeat_n(0, pool.colorless() as usize));
1342
1343 macro_rules! avail_add {
1345 ($avail:expr, $is_snow:expr, $atom:expr) => {
1346 if $is_snow {
1347 $avail.add_snow($atom, 1);
1348 } else {
1349 $avail.add($atom, 1);
1350 }
1351 };
1352 }
1353
1354 for card_id in battlefield.iter().copied().chain(hand_cards.into_iter()) {
1355 if excluded_source == Some(card_id) {
1356 continue;
1357 }
1358 let card = game.card(card_id);
1359 let is_tapped = card.tapped;
1360 let card_is_snow = card.type_line.is_snow();
1361
1362 let summoning_sick = card.is_creature() && card.summoning_sick && !card.has_haste();
1367 if summoning_sick {
1368 let all_need_tap = card
1369 .activated_abilities
1370 .iter()
1371 .filter(|ab| ab.is_mana_ability)
1372 .all(|ab| ab.cost.parts.iter().any(|p| matches!(p, CostPart::Tap)));
1373 if all_need_tap {
1374 continue;
1375 }
1376 }
1377
1378 let mana_abilities: Vec<_> = card
1384 .activated_abilities
1385 .iter()
1386 .filter(|ab| {
1387 ab.is_mana_ability
1388 && match card.zone {
1389 ZoneType::Battlefield => ab.activation_zone != Some(ZoneType::Hand),
1390 ZoneType::Hand => ab.activation_zone == Some(ZoneType::Hand),
1391 _ => false,
1392 }
1393 && !ab.cost.parts.iter().any(|p| matches!(p, CostPart::Mana { .. }))
1394 && (!is_tapped || !ab.cost.parts.iter().any(|p| matches!(p, CostPart::Tap)))
1395 && (!summoning_sick
1396 || !ab.cost.parts.iter().any(|p| matches!(p, CostPart::Tap)))
1397 && crate::cost::can_pay_ignoring_mana(&ab.cost, game, card_id, player)
1401 && crate::game_loop::GameLoop::mana_ability_available_for_payment_with_reserved(
1402 game,
1403 player,
1404 card_id,
1405 ab,
1406 reserved_sacrifices,
1407 )
1408 && {
1413 if let Some(ctx) = payment_ctx {
1414 match ab.restrict_valid.as_deref() {
1415 Some(raw) => {
1416 let resolved = if raw.contains("ChosenType") {
1417 let chosen = card
1418 .chosen_type
1419 .clone()
1420 .unwrap_or_default();
1421 raw.replace("ChosenType", &chosen)
1422 } else {
1423 raw.to_string()
1424 };
1425 mana_meets_restriction(&resolved, ctx)
1426 }
1427 None => true,
1428 }
1429 } else {
1430 true
1431 }
1432 }
1433 })
1434 .collect();
1435
1436 if mana_abilities.is_empty() {
1437 if card.zone == ZoneType::Battlefield && card.is_land() && !is_tapped {
1443 let subtype_atoms = all_basic_subtype_atoms(card);
1444 if !subtype_atoms.is_empty() {
1445 let mut src_mask: u16 = 0;
1446 let mut source_units = 0usize;
1447 for atom in subtype_atoms {
1448 let adjusted_atoms = replacement_adjusted_atoms_for_availability(
1449 game, player, card_id, atom,
1450 );
1451 source_units = source_units.max(adjusted_atoms.len());
1452 for adjusted_atom in adjusted_atoms {
1453 avail_add!(available, card_is_snow, adjusted_atom);
1454 src_mask |= adjusted_atom;
1455 }
1456 }
1457 for _ in 0..source_units.max(1) {
1458 source_count += 1;
1459 source_colors.push(src_mask);
1460 }
1461 add_taps_for_mana_trigger_mana_for_availability(
1462 &mut available,
1463 &mut source_count,
1464 &mut source_colors,
1465 game,
1466 player,
1467 card_id,
1468 &atoms_mask_to_letters(src_mask),
1469 );
1470 } else if let Some(atom) = basic_land_mana_atom(card) {
1471 let adjusted_atoms =
1472 replacement_adjusted_atoms_for_availability(game, player, card_id, atom);
1473 let mut src_mask: u16 = 0;
1474 for adjusted_atom in &adjusted_atoms {
1475 avail_add!(available, card_is_snow, *adjusted_atom);
1476 src_mask |= *adjusted_atom;
1477 }
1478 for _ in 0..adjusted_atoms.len().max(1) {
1479 source_count += 1;
1480 source_colors.push(src_mask);
1481 }
1482 add_taps_for_mana_trigger_mana_for_availability(
1483 &mut available,
1484 &mut source_count,
1485 &mut source_colors,
1486 game,
1487 player,
1488 card_id,
1489 &atoms_mask_to_letters(src_mask),
1490 );
1491 }
1492 }
1493 continue;
1494 }
1495
1496 let mut added_any = false;
1499 let mut counted_fixed_output = false;
1500 let mut added_atoms: Vec<u16> = Vec::new();
1501 let mut src_mask: u16 = 0;
1502 for ab in &mana_abilities {
1503 if ab.is_mana_reflected {
1507 let reflected_atoms = reflected_atoms_for_availability(game, player, card_id, ab);
1508 let amount = resolve_mana_ability_amount(game, card_id, player, ab);
1510 for &atom in &reflected_atoms {
1511 if !added_atoms.contains(&atom) {
1512 for _ in 0..amount {
1513 avail_add!(available, card_is_snow, atom);
1514 }
1515 added_atoms.push(atom);
1516 src_mask |= atom;
1517 added_any = true;
1518 }
1519 }
1520 if amount > 1 && !reflected_atoms.is_empty() {
1524 for _ in 0..(amount - 1) {
1528 source_count += 1;
1529 source_colors.push(src_mask);
1530 }
1531 }
1532 } else if let Some(produced_ir) = ab.produced_ir.as_ref() {
1533 if produced_ir.is_combo_color_identity() {
1534 let colors = game.player_commander_color_identity(player);
1536 if !colors.is_empty() {
1537 for atom in chosen_colors_to_atoms(&colors) {
1538 if !added_atoms.contains(&atom) {
1539 avail_add!(available, card_is_snow, atom);
1540 added_atoms.push(atom);
1541 src_mask |= atom;
1542 }
1543 }
1544 added_any = true;
1545 }
1546 } else if let Some(special) =
1547 ab.produced_ir.as_ref().and_then(ProducedMana::special_kind)
1548 {
1549 let special_atoms =
1553 crate::ability::effects::mana_effect::available_special_mana_atoms(
1554 game, card_id, player, special,
1555 );
1556 if !special_atoms.is_empty() {
1557 for &atom in &special_atoms {
1558 if !added_atoms.contains(&atom) {
1559 let adjusted_atoms = replacement_adjusted_atoms_for_availability(
1560 game, player, card_id, atom,
1561 );
1562 for adjusted_atom in adjusted_atoms {
1563 avail_add!(available, card_is_snow, adjusted_atom);
1564 src_mask |= adjusted_atom;
1565 source_count += 1;
1566 source_colors.push(adjusted_atom);
1567 }
1568 added_atoms.push(atom);
1569 }
1570 }
1571 added_any = true;
1572 counted_fixed_output = true;
1580 }
1581 } else {
1582 let amount = resolve_mana_ability_amount(game, card_id, player, ab);
1583 let mut counted_variable_source_units = false;
1584 if let Some(fixed_atoms) = produced_ir.fixed_atoms() {
1585 for atom in fixed_atoms {
1586 for _ in 0..amount {
1587 let adjusted_atoms = replacement_adjusted_atoms_for_availability(
1588 game, player, card_id, atom,
1589 );
1590 for adjusted_atom in adjusted_atoms {
1591 avail_add!(available, card_is_snow, adjusted_atom);
1592 src_mask |= adjusted_atom;
1593 source_count += 1;
1594 source_colors.push(adjusted_atom);
1595 }
1596 }
1597 added_any = true;
1598 counted_fixed_output = true;
1599 }
1600 } else {
1601 let mut source_units = 0usize;
1602 let intrinsic = produced_ir.to_atoms(&card.chosen_colors);
1603 let allowed = java_replacement_filtered_atoms_for_availability(
1604 game, player, card_id, ab, &intrinsic,
1605 );
1606 for atom in allowed {
1607 if !added_atoms.contains(&atom) {
1608 for _ in 0..amount {
1609 let adjusted_atoms =
1610 replacement_adjusted_atoms_for_availability(
1611 game, player, card_id, atom,
1612 );
1613 source_units =
1614 source_units.max(adjusted_atoms.len() * amount as usize);
1615 for adjusted_atom in adjusted_atoms {
1616 avail_add!(available, card_is_snow, adjusted_atom);
1617 src_mask |= adjusted_atom;
1618 }
1619 }
1620 added_atoms.push(atom);
1621 added_any = true;
1622 }
1623 }
1624 if source_units > 1 && added_any {
1625 let intrinsic_amount = amount > 1;
1643 let colored_bits = (src_mask
1644 & (ManaAtom::WHITE
1645 | ManaAtom::BLUE
1646 | ManaAtom::BLACK
1647 | ManaAtom::RED
1648 | ManaAtom::GREEN))
1649 .count_ones();
1650 let extra_mask = if intrinsic_amount {
1651 src_mask
1652 } else if colored_bits > 1 {
1653 ManaAtom::COLORLESS
1654 } else {
1655 src_mask
1656 };
1657 for _ in 0..(source_units as i32 - 1) {
1658 source_count += 1;
1659 source_colors.push(extra_mask);
1660 }
1661 counted_variable_source_units = true;
1662 }
1663 }
1664 if amount > 1
1669 && added_any
1670 && !counted_fixed_output
1671 && !counted_variable_source_units
1672 {
1673 for _ in 0..(amount - 1) {
1674 source_count += 1;
1675 source_colors.push(src_mask);
1676 }
1677 }
1678 }
1679 }
1680 }
1681 if !added_any && card.is_land() {
1682 let subtype_atoms = all_basic_subtype_atoms(card);
1687 if !subtype_atoms.is_empty() {
1688 for atom in subtype_atoms {
1689 if !added_atoms.contains(&atom) {
1690 avail_add!(available, card_is_snow, atom);
1691 added_atoms.push(atom);
1692 src_mask |= atom;
1693 added_any = true;
1694 }
1695 }
1696 } else if let Some(atom) = basic_land_mana_atom(card) {
1697 avail_add!(available, card_is_snow, atom);
1699 src_mask |= atom;
1700 added_any = true;
1701 }
1702 }
1703 if added_any && !counted_fixed_output {
1704 source_count += 1;
1706 source_colors.push(src_mask);
1707 }
1708 if added_any {
1709 let combined_mask = added_atoms.iter().fold(src_mask, |mask, atom| mask | atom);
1710 add_taps_for_mana_trigger_mana_for_availability(
1711 &mut available,
1712 &mut source_count,
1713 &mut source_colors,
1714 game,
1715 player,
1716 card_id,
1717 &atoms_mask_to_letters(combined_mask),
1718 );
1719 }
1720 }
1721
1722 for &card_id in battlefield {
1727 let card = game.card(card_id);
1728 if card.tapped || card.attachments.is_empty() {
1729 continue;
1730 }
1731 for &aura_id in &card.attachments {
1732 if aura_id.index() >= game.cards.len() {
1733 continue;
1734 }
1735 let aura = game.card(aura_id);
1736 if aura.zone != ZoneType::Battlefield {
1737 continue;
1738 }
1739 for trigger in &aura.triggers {
1740 if trigger.kind == crate::trigger::TriggerType::TapsForMana {
1741 if let Some(svar_text) = aura.svars.get(&trigger.execute) {
1744 let params = crate::parsing::Params::from_raw(svar_text);
1745 if let Some(produced) = params.get(crate::parsing::keys::PRODUCED) {
1746 let produced_ir = ProducedMana::from_raw_boundary(produced);
1747 let atoms = if matches!(produced_ir, ProducedMana::Chosen) {
1748 aura.chosen_colors
1750 .first()
1751 .and_then(|c| color_name_to_mana_atom(c))
1752 .into_iter()
1753 .collect::<Vec<_>>()
1754 } else {
1755 produced_ir.to_atoms(&aura.chosen_colors)
1756 };
1757 for atom in atoms {
1758 available.add(atom, 1);
1759 source_count += 1;
1760 source_colors.push(atom);
1761 }
1762 }
1763 }
1764 }
1765 }
1766 }
1767 }
1768
1769 available.total_sources = Some(pool.total_mana() + source_count);
1771 available.source_colors = Some(source_colors);
1772
1773 available
1774}
1775
1776pub(crate) fn resolve_mana_ability_amount(
1781 game: &GameState,
1782 card_id: CardId,
1783 player: PlayerId,
1784 ab: &crate::ability::activated::ActivatedAbility,
1785) -> i32 {
1786 let amount_str = match ab.amount.as_deref() {
1787 Some(v) if !v.is_empty() => v,
1788 _ => return 1,
1789 };
1790 if let Ok(n) = amount_str.trim().parse::<i32>() {
1792 return n.max(1);
1793 }
1794 let card = game.card(card_id);
1796 if let Some(svar_expr) = card.svars.get(amount_str.trim()) {
1797 if svar_expr.starts_with("Count$") {
1798 return crate::ability::effects::resolve_count_svar(svar_expr, game, card_id, player)
1799 .max(1);
1800 }
1801 if let Ok(n) = svar_expr.trim().parse::<i32>() {
1802 return n.max(1);
1803 }
1804 }
1805 1
1806}
1807
1808pub(crate) fn mana_ability_prompt_metadata(
1809 game: &GameState,
1810 card_id: CardId,
1811 player: PlayerId,
1812 ab: &crate::ability::activated::ActivatedAbility,
1813) -> (Option<String>, Option<i32>) {
1814 let produced_mana = ab.produced_ir.as_ref().map(|produced_ir| {
1815 let chosen_colors = &game.card(card_id).chosen_colors;
1816 let atoms = produced_ir.to_atoms(chosen_colors);
1817 if atoms.is_empty() {
1818 produced_ir.as_script_text().into_owned()
1819 } else if matches!(produced_ir, ProducedMana::Chosen) {
1820 atoms
1821 .into_iter()
1822 .map(|atom| ManaPool::atom_to_letter(atom).to_string())
1823 .collect::<Vec<_>>()
1824 .join(" ")
1825 } else if matches!(produced_ir, ProducedMana::Combo(ProducedManaCombo::Chosen)) {
1826 format!(
1827 "Combo {}",
1828 atoms
1829 .into_iter()
1830 .map(|atom| ManaPool::atom_to_letter(atom).to_string())
1831 .collect::<Vec<_>>()
1832 .join(" ")
1833 )
1834 } else {
1835 produced_ir.as_script_text().into_owned()
1836 }
1837 });
1838 let produced_mana_amount = Some(resolve_mana_ability_amount(game, card_id, player, ab));
1839 (produced_mana, produced_mana_amount)
1840}
1841
1842#[cfg(test)]
1843mod tests {
1844 use super::*;
1845 use crate::agent::{PassAgent, PlayerAgent};
1846 use crate::card::Card;
1847 use crate::game::GameState;
1848 use crate::ids::{CardId, PlayerId};
1849 use forge_foundation::ManaCost;
1850 use forge_foundation::{CardTypeLine, ColorSet, ZoneType};
1851
1852 #[test]
1853 fn basic_land_detection() {
1854 use crate::card::Card;
1855 use crate::ids::{CardId, PlayerId};
1856 use forge_foundation::ColorSet;
1857
1858 let card = Card::new(
1859 CardId(0),
1860 "Mountain".to_string(),
1861 PlayerId(0),
1862 forge_foundation::CardTypeLine::parse("Basic Land - Mountain"),
1863 ManaCost::no_cost(),
1864 ColorSet::COLORLESS,
1865 None,
1866 None,
1867 vec![],
1868 vec![],
1869 );
1870 assert_eq!(basic_land_mana_atom(&card), Some(ManaAtom::RED));
1871 }
1872
1873 #[test]
1874 fn mana_atom_from_produced_test() {
1875 assert_eq!(mana_atom_from_produced("W"), Some(ManaAtom::WHITE));
1876 assert_eq!(mana_atom_from_produced("U"), Some(ManaAtom::BLUE));
1877 assert_eq!(mana_atom_from_produced("B"), Some(ManaAtom::BLACK));
1878 assert_eq!(mana_atom_from_produced("R"), Some(ManaAtom::RED));
1879 assert_eq!(mana_atom_from_produced("G"), Some(ManaAtom::GREEN));
1880 assert_eq!(mana_atom_from_produced("C"), Some(ManaAtom::COLORLESS));
1881 assert_eq!(mana_atom_from_produced("X"), None);
1882 }
1883
1884 #[test]
1885 fn produced_to_atoms_any_and_combo_any() {
1886 let any = ProducedMana::Any.to_atoms(&[]);
1887 assert!(any.contains(&ManaAtom::WHITE));
1888 assert!(any.contains(&ManaAtom::BLUE));
1889 assert!(any.contains(&ManaAtom::BLACK));
1890 assert!(any.contains(&ManaAtom::RED));
1891 assert!(any.contains(&ManaAtom::GREEN));
1892 assert!(!any.contains(&ManaAtom::COLORLESS));
1893
1894 let combo_any = ProducedMana::Combo(ProducedManaCombo::Any).to_atoms(&[]);
1895 assert_eq!(any.len(), combo_any.len());
1896 for a in any {
1897 assert!(combo_any.contains(&a));
1898 }
1899 }
1900
1901 #[test]
1902 fn produced_to_atoms_chosen_and_combo_chosen() {
1903 let chosen = vec!["Red".to_string(), "Green".to_string()];
1904 let a = ProducedMana::Chosen.to_atoms(&chosen);
1905 assert!(a.contains(&ManaAtom::RED));
1906 assert!(a.contains(&ManaAtom::GREEN));
1907 assert_eq!(a.len(), 2);
1908
1909 let b = ProducedMana::Combo(ProducedManaCombo::Chosen).to_atoms(&chosen);
1910 assert!(b.contains(&ManaAtom::RED));
1911 assert!(b.contains(&ManaAtom::GREEN));
1912 assert_eq!(b.len(), 2);
1913 }
1914
1915 #[test]
1916 fn produced_to_atoms_multi_token_fixed_output() {
1917 let atoms = ProducedMana::Fixed(vec!["C".to_string(), "C".to_string()]).to_atoms(&[]);
1918 assert_eq!(atoms, vec![ManaAtom::COLORLESS]);
1919 }
1920
1921 #[test]
1922 fn pay_simple_cost() {
1923 let mut pool = ManaPool::new();
1924 pool.add(ManaAtom::RED, 1);
1925
1926 let cost = ManaCost::parse("R");
1927 assert!(pool.can_pay(&cost));
1928 assert!(pool.try_pay(&cost));
1929 assert_eq!(pool.red(), 0);
1930 }
1931
1932 #[test]
1933 fn pay_generic_and_colored() {
1934 let mut pool = ManaPool::new();
1935 pool.add(ManaAtom::GREEN, 2);
1936
1937 let cost = ManaCost::parse("1 G");
1938 assert!(pool.can_pay(&cost));
1939 assert!(pool.try_pay(&cost));
1940 assert_eq!(pool.green(), 0); }
1942
1943 #[test]
1944 fn insufficient_mana() {
1945 let mut pool = ManaPool::new();
1946 pool.add(ManaAtom::RED, 1);
1947
1948 let cost = ManaCost::parse("1 R R");
1949 assert!(!pool.can_pay(&cost));
1950 }
1951
1952 #[test]
1953 fn empty_pool() {
1954 let mut pool = ManaPool::new();
1955 pool.add(ManaAtom::WHITE, 3);
1956 pool.reset_pool();
1957 assert_eq!(pool.total_mana(), 0);
1958 }
1959
1960 #[test]
1961 fn combo_color_identity_uses_registered_commander_outside_command_zone() {
1962 let mut game = GameState::new(&["P1", "P2"], 20);
1963 let p0 = PlayerId(0);
1964
1965 let commander = Card::new(
1966 CardId(0),
1967 "Commander".to_string(),
1968 p0,
1969 CardTypeLine::parse("Legendary Creature Wizard"),
1970 ManaCost::parse("2 U"),
1971 ColorSet::BLUE,
1972 Some(2),
1973 Some(2),
1974 vec![],
1975 vec![],
1976 );
1977 let commander_id = game.create_card(commander);
1978 game.player_register_commander(p0, commander_id);
1979 game.player_create_commander_effect(p0, None);
1980 game.move_card(commander_id, ZoneType::Battlefield, p0);
1981
1982 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![Box::new(PassAgent), Box::new(PassAgent)];
1983 let produced = determine_mana_production_ir(
1984 &mut game,
1985 &mut agents,
1986 p0,
1987 commander_id,
1988 &ProducedMana::Combo(ProducedManaCombo::ColorIdentity),
1989 "Combo ColorIdentity",
1990 None,
1991 None,
1992 );
1993
1994 assert_eq!(produced.as_deref(), Some("U"));
1995 }
1996
1997 #[test]
1998 fn restricted_mana_can_use_source_chosen_type_for_creature_spells() {
1999 let mut pool = ManaPool::new();
2000 let source = CardId(7);
2001 let mut mana = Mana::simple(ManaAtom::BLACK);
2002 mana.source_card = Some(source);
2003 mana.restriction = Some("Spell.Creature+ChosenType".to_string());
2004 pool.add_mana(mana);
2005
2006 let mut chosen_types_by_source = std::collections::HashMap::new();
2007 chosen_types_by_source.insert(source, "Assassin".to_string());
2008
2009 let ctx = ManaPaymentContext {
2010 is_spell: true,
2011 is_activated_ability: false,
2012 sa_on_stack: false,
2013 type_line: Some(CardTypeLine::parse("Creature Zombie Assassin")),
2014 card_name: Some("Unstoppable Slasher".to_string()),
2015 card_color: None,
2016 chosen_types_by_source,
2017 };
2018
2019 assert!(pool.can_pay_for_spell(&ManaCost::parse("B"), &ctx));
2020 assert!(pool.try_pay_for_spell(&ManaCost::parse("B"), &ctx));
2021 assert_eq!(pool.total_mana(), 0);
2022 }
2023}