manabrew_engine/ability/effects/
mana_effect.rs1use forge_foundation::mana::ManaAtom;
2
3use super::{mana_atom_from_produced, EffectContext};
4use crate::spellability::SpellAbility;
5
6pub fn build_spell_ability(sa: &mut crate::spellability::SpellAbility) {
12 if !sa.uses_targeting() {
15 sa.is_mana_ability = true;
16 }
17}
18
19pub fn handle_special_mana(
24 ctx: &mut EffectContext,
25 sa: &crate::spellability::SpellAbility,
26 source_id: crate::ids::CardId,
27 player: crate::ids::PlayerId,
28 special: &str,
29) -> Vec<String> {
30 resolve_special_mana(ctx, sa, source_id, player, special)
31}
32
33#[manabrew_engine_macros::spell_effect(ManaEffect)]
39fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
40 let player = sa.activating_player;
41 let source_id = match sa.source {
42 Some(id) => id,
43 None => return,
44 };
45
46 let produced_ir = match sa.produced_ir() {
47 Some(ir) => ir,
48 None => return,
49 };
50 let produced = produced_ir.as_script_text();
51
52 if sa.ir.optional {
55 let card_name = ctx.game.card(source_id).card_name.clone();
56 if !ctx.agents[player.index()].confirm_action(
57 player,
58 Some("ProduceMana"),
59 "Produce mana?",
60 &[],
61 Some(source_id),
62 sa.api,
63 ) {
64 return;
65 }
66 }
67
68 let chooser = sa
71 .chooser()
72 .and_then(|d| super::resolve_defined_player(d, player, ctx.game))
73 .unwrap_or(player);
74
75 let restriction = sa.ir.restrict_valid.clone().map(|r| {
79 if r.contains("ChosenType") {
80 let chosen = ctx
81 .game
82 .card(source_id)
83 .chosen_type
84 .clone()
85 .unwrap_or_default();
86 r.replace("ChosenType", &chosen)
87 } else {
88 r
89 }
90 });
91 let adds_no_counter = sa.ir.adds_no_counter;
92 let adds_keywords = sa.ir.adds_keywords.clone();
93 let adds_keywords_valid = sa.ir.adds_keywords_valid.clone();
94 let adds_counters = sa.ir.adds_counters.clone();
95 let adds_counters_valid = sa.ir.adds_counters_valid.clone();
96 let triggers_when_spent = sa.ir.triggers_when_spent.clone();
97 let Some(produced_ir) = sa.produced_ir().cloned() else {
98 return;
99 };
100
101 if let Some(special) = produced_ir.special_kind() {
103 let mana_tokens = resolve_special_mana(ctx, sa, source_id, player, special);
104 if mana_tokens.is_empty() {
105 return;
106 }
107 let source_is_snow = ctx.game.card(source_id).type_line.is_snow();
108 for tok in &mana_tokens {
109 if let Some(atom) = mana_atom_from_produced(tok) {
110 let mut m = crate::mana::Mana::simple(atom);
111 m.source_card = Some(source_id);
112 m.is_snow = source_is_snow;
113 m.restriction = restriction.clone();
114 m.adds_no_counter = adds_no_counter;
115 m.adds_keywords = adds_keywords.clone();
116 m.adds_keywords_valid = adds_keywords_valid.clone();
117 m.adds_counters = adds_counters.clone();
118 m.adds_counters_valid = adds_counters_valid.clone();
119 m.triggers_when_spent = triggers_when_spent.clone();
120 ctx.mana_pools[player.index()].add_mana(m);
121 }
122 }
123 return;
124 }
125
126 let mana_string: Option<String> = if produced_ir.is_any_like() {
128 let available = [
130 "W".to_string(),
131 "U".to_string(),
132 "B".to_string(),
133 "R".to_string(),
134 "G".to_string(),
135 ];
136 Some(available.first().cloned().unwrap_or("W".to_string()))
137 } else if produced_ir.is_choice_like() {
138 let options: Vec<&str> = if let Some(rest) = produced.strip_prefix("Combo ") {
140 rest.split_whitespace().collect()
141 } else {
142 produced.split(',').map(|s| s.trim()).collect()
143 };
144 let available: Vec<String> = options
145 .iter()
146 .filter_map(|name| {
147 let lower = name.to_lowercase();
148 match lower.as_str() {
149 "white" | "w" => Some("W".to_string()),
150 "blue" | "u" => Some("U".to_string()),
151 "black" | "b" => Some("B".to_string()),
152 "red" | "r" => Some("R".to_string()),
153 "green" | "g" => Some("G".to_string()),
154 "colorless" | "c" => Some("C".to_string()),
155 _ => mana_atom_from_produced(name).map(|_| name.to_string()),
156 }
157 })
158 .collect();
159 available
160 .first()
161 .cloned()
162 .or_else(|| Some(produced.to_string()))
163 } else if produced == "Chosen" {
164 let chosen = ctx
166 .game
167 .card(source_id)
168 .chosen_colors
169 .first()
170 .cloned()
171 .unwrap_or_else(|| "White".to_string());
172 let lower = chosen.to_lowercase();
173 match lower.as_str() {
174 "white" => Some("W".to_string()),
175 "blue" => Some("U".to_string()),
176 "black" => Some("B".to_string()),
177 "red" => Some("R".to_string()),
178 "green" => Some("G".to_string()),
179 _ => Some("C".to_string()),
180 }
181 } else {
182 Some(produced.to_string())
184 };
185
186 let mut final_mana = match mana_string {
188 Some(ms) => ms,
189 None => return,
190 };
191 let amount = super::resolve_numeric_svar(ctx.game, sa, "Amount", 1);
192 if amount <= 0 {
193 return;
194 }
195 let is_combo = produced_ir.is_choice_like();
196 if amount > 1 && is_combo {
197 let available: Vec<String> = if produced_ir.is_any_like() {
202 vec!["W", "U", "B", "R", "G"]
203 .into_iter()
204 .map(String::from)
205 .collect()
206 } else {
207 let options: Vec<&str> = if let Some(rest) = produced.strip_prefix("Combo ") {
208 rest.split_whitespace().collect()
209 } else {
210 produced.split(',').map(|s| s.trim()).collect()
211 };
212 options
213 .iter()
214 .filter_map(|name| {
215 let lower = name.to_lowercase();
216 match lower.as_str() {
217 "white" | "w" => Some("W".to_string()),
218 "blue" | "u" => Some("U".to_string()),
219 "black" | "b" => Some("B".to_string()),
220 "red" | "r" => Some("R".to_string()),
221 "green" | "g" => Some("G".to_string()),
222 "colorless" | "c" => Some("C".to_string()),
223 _ => None,
224 }
225 })
226 .collect()
227 };
228 let mut per_unit: Vec<String> = Vec::with_capacity(amount as usize);
229 for _ in 0..amount {
230 let pick = ctx.agents[chooser.index()]
231 .choose_color(chooser, &available)
232 .or_else(|| available.first().cloned())
233 .unwrap_or_else(|| "C".to_string());
234 per_unit.push(pick);
235 }
236 let card_name = ctx.game.card(source_id).card_name.clone();
240 let _summary = ctx.agents[chooser.index()].specify_mana_combo(
241 chooser,
242 &per_unit,
243 amount as usize,
244 Some(source_id),
245 None,
246 );
247 final_mana = per_unit.join(" ");
248 } else if amount > 1 {
249 let base = final_mana.clone();
250 for _ in 1..amount {
251 final_mana.push(' ');
252 final_mana.push_str(&base);
253 }
254 }
255
256 {
258 use crate::replacement::replacement_handler::{apply_replacements, ReplacementEvent};
259 let mut event = ReplacementEvent::ProduceMana {
260 source: source_id,
261 activator: player,
262 mana: final_mana.clone(),
263 };
264 let result = apply_replacements(ctx.game, &mut event);
265 if result == crate::replacement::ReplacementResult::Updated {
266 if let ReplacementEvent::ProduceMana { mana: new_mana, .. } = event {
267 final_mana = new_mana;
268 }
269 }
270 }
271
272 let source_is_snow = ctx.game.card(source_id).type_line.is_snow();
274
275 for tok in final_mana.split_whitespace() {
277 if let Some(atom) = mana_atom_from_produced(tok) {
278 let mut m = crate::mana::Mana::simple(atom);
279 m.source_card = Some(source_id);
280 m.is_snow = source_is_snow;
281 m.restriction = restriction.clone();
282 m.adds_no_counter = adds_no_counter;
283 m.adds_keywords = adds_keywords.clone();
284 m.adds_keywords_valid = adds_keywords_valid.clone();
285 m.adds_counters = adds_counters.clone();
286 m.adds_counters_valid = adds_counters_valid.clone();
287 m.triggers_when_spent = triggers_when_spent.clone();
288 ctx.mana_pools[player.index()].add_mana(m);
289 }
290 }
291}
292
293pub fn resolve_special_mana(
296 ctx: &mut EffectContext,
297 sa: &SpellAbility,
298 source_id: crate::ids::CardId,
299 player: crate::ids::PlayerId,
300 special: &str,
301) -> Vec<String> {
302 use forge_foundation::ZoneType;
303
304 if special.starts_with("EachColorAmong_Valid") {
305 let filter = special
307 .strip_prefix("EachColorAmong_Valid ")
308 .unwrap_or("Permanent.YouCtrl");
309 return each_color_among_valid(ctx.game, source_id, player, filter);
310 }
311
312 if special == "EachColorAmong_ExiledWith" {
313 let mut colors = Vec::new();
315 for card in ctx.game.cards.iter() {
316 if card.zone != ZoneType::Exile {
317 continue;
318 }
319 if !card.remembered_cards.contains(&source_id) {
321 continue;
322 }
323 for color in card.color.iter() {
324 let letter = color_to_letter(color.long_name());
325 if !letter.is_empty() && !colors.contains(&letter) {
326 colors.push(letter);
327 }
328 }
329 }
330 return colors;
331 }
332
333 if special.starts_with("EachColoredManaSymbol") {
334 if special.contains("Milled") {
337 let gy = ctx.game.cards_in_zone(ZoneType::Graveyard, player);
338 if let Some(&last_card_id) = gy.last() {
339 let milled = ctx.game.card(last_card_id);
340 let mut tokens = Vec::new();
341 for &shard in milled.mana_cost.shards() {
342 let atoms = shard.shard();
343 if (atoms & ManaAtom::WHITE) != 0 {
344 tokens.push("W".to_string());
345 }
346 if (atoms & ManaAtom::BLUE) != 0 {
347 tokens.push("U".to_string());
348 }
349 if (atoms & ManaAtom::BLACK) != 0 {
350 tokens.push("B".to_string());
351 }
352 if (atoms & ManaAtom::RED) != 0 {
353 tokens.push("R".to_string());
354 }
355 if (atoms & ManaAtom::GREEN) != 0 {
356 tokens.push("G".to_string());
357 }
358 }
359 return tokens;
360 }
361 }
362 return vec![];
363 }
364
365 if special == "DoubleManaInPool" {
366 let pool = &ctx.mana_pools[player.index()];
368 let mut tokens = Vec::new();
369 for &(atom, letter) in &[
370 (ManaAtom::WHITE, "W"),
371 (ManaAtom::BLUE, "U"),
372 (ManaAtom::BLACK, "B"),
373 (ManaAtom::RED, "R"),
374 (ManaAtom::GREEN, "G"),
375 (ManaAtom::COLORLESS, "C"),
376 ] {
377 let count = pool.count_color(atom);
378 for _ in 0..count {
379 tokens.push(letter.to_string());
380 }
381 }
382 return tokens;
383 }
384
385 if special == "EnchantedManaCost" {
386 let source = ctx.game.card(source_id);
388 if let Some(attached_to) = source.attached_to {
389 let enchanted = ctx.game.card(attached_to);
390 let mut tokens = Vec::new();
391 for &shard in enchanted.mana_cost.shards() {
392 let atoms = shard.shard();
393 if (atoms & ManaAtom::WHITE) != 0 {
394 tokens.push("W".to_string());
395 }
396 if (atoms & ManaAtom::BLUE) != 0 {
397 tokens.push("U".to_string());
398 }
399 if (atoms & ManaAtom::BLACK) != 0 {
400 tokens.push("B".to_string());
401 }
402 if (atoms & ManaAtom::RED) != 0 {
403 tokens.push("R".to_string());
404 }
405 if (atoms & ManaAtom::GREEN) != 0 {
406 tokens.push("G".to_string());
407 }
408 }
409 let generic = enchanted.mana_cost.generic_cost();
411 for _ in 0..generic {
412 tokens.push("C".to_string());
413 }
414 return tokens;
415 }
416 return vec![];
417 }
418
419 if special.starts_with("LastNotedType") {
420 let source = ctx.game.card(source_id);
422 if let Some(noted) = source.chosen_colors.first() {
423 let letter = color_to_letter(¬ed.to_lowercase());
424 if !letter.is_empty() {
425 let amount = super::resolve_numeric_svar(ctx.game, sa, "Amount", 1);
427 let mut tokens = Vec::new();
428 for _ in 0..amount {
429 tokens.push(letter.clone());
430 }
431 return tokens;
432 }
433 }
434 return vec![];
435 }
436
437 vec![]
438}
439
440pub fn each_color_among_valid(
442 game: &crate::game::GameState,
443 source_id: crate::ids::CardId,
444 controller: crate::ids::PlayerId,
445 filter: &str,
446) -> Vec<String> {
447 use forge_foundation::ZoneType;
448
449 let mut colors = Vec::new();
450
451 for card in game.cards.iter() {
452 if card.zone != ZoneType::Battlefield {
453 continue;
454 }
455 if !matches_simple_valid(card, filter, source_id, controller) {
456 continue;
457 }
458 for color in card.color.iter() {
459 let letter = color_to_letter(color.long_name());
460 if !letter.is_empty() && !colors.contains(&letter) {
461 colors.push(letter);
462 }
463 }
464 }
465
466 colors
467}
468
469pub fn available_special_mana_atoms(
477 game: &crate::game::GameState,
478 source_id: crate::ids::CardId,
479 controller: crate::ids::PlayerId,
480 special: &str,
481) -> Vec<u16> {
482 let mut atoms = Vec::new();
483
484 if special.starts_with("EachColorAmong_Valid") {
485 let filter = special
486 .strip_prefix("EachColorAmong_Valid ")
487 .unwrap_or("Permanent.YouCtrl");
488 for letter in each_color_among_valid(game, source_id, controller, filter) {
489 if let Some(atom) = crate::mana::mana_atom_from_produced(&letter) {
490 atoms.push(atom);
491 }
492 }
493 }
494
495 atoms
496}
497
498fn matches_simple_valid(
500 card: &crate::card::Card,
501 filter: &str,
502 source_id: crate::ids::CardId,
503 controller: crate::ids::PlayerId,
504) -> bool {
505 let parts: Vec<&str> = filter.split('.').collect();
506 let base = parts[0];
507
508 let type_ok = match base {
509 "Permanent" | "Card" => true,
510 "Creature" => card.is_creature(),
511 "Land" => card.type_line.is_land(),
512 "Artifact" => card.type_line.is_artifact(),
513 "Enchantment" => card.type_line.is_enchantment(),
514 _ => card.type_line.has_subtype(base),
515 };
516 if !type_ok {
517 return false;
518 }
519
520 for &qualifier in parts.iter().skip(1) {
521 for q in qualifier.split('+') {
522 match q {
523 "YouCtrl" => {
524 if card.controller != controller {
525 return false;
526 }
527 }
528 "OppCtrl" => {
529 if card.controller == controller {
530 return false;
531 }
532 }
533 "MonoColor" => {
534 if card.color.count_colors() != 1 {
536 return false;
537 }
538 }
539 "Self" => {
540 if card.id != source_id {
541 return false;
542 }
543 }
544 "Legendary" => {
545 if !card.type_line.is_legendary() {
546 return false;
547 }
548 }
549 _ => {}
550 }
551 }
552 }
553
554 true
555}
556
557fn color_to_letter(color_name: &str) -> String {
558 match color_name {
559 "white" => "W".to_string(),
560 "blue" => "U".to_string(),
561 "black" => "B".to_string(),
562 "red" => "R".to_string(),
563 "green" => "G".to_string(),
564 _ => String::new(),
565 }
566}