Skip to main content

manabrew_engine/ability/effects/
protect_effect.rs

1use forge_foundation::ZoneType;
2
3use super::EffectContext;
4use crate::ability::ability_ir::DefinedRef;
5use crate::card::card_util;
6use crate::spellability::SpellAbility;
7
8/// Return the list of qualities a `SP$ Protection | Gains$ Choice` ability can
9/// choose from. Mirrors Java `ProtectEffect.getProtectionList(SpellAbility)`.
10///
11/// When `Choices$` contains `AnyColor`, expands to the five MTG colors. When
12/// `Choices$` contains `CardType`, Java expands to every card type — not yet
13/// implemented here (no `CardType::all()` enumerator). Everything else is a
14/// comma-separated list.
15pub fn get_protection_list(sa: &SpellAbility) -> Vec<String> {
16    let gains = sa.ir.gains.as_deref().unwrap_or("");
17    if gains != "Choice" && !gains.contains("chosen color") {
18        return gains
19            .split(',')
20            .map(|c| c.trim().to_string())
21            .filter(|s| !s.is_empty())
22            .collect();
23    }
24
25    let mut out = Vec::new();
26    let choices = sa.ir.choices.as_deref().unwrap_or("");
27    let mut choices_mut = choices.to_string();
28    if choices_mut.contains("AnyColor") {
29        out.extend(
30            ["White", "Blue", "Black", "Red", "Green"]
31                .iter()
32                .map(|s| s.to_string()),
33        );
34        choices_mut = choices_mut.replace("AnyColor,", "").replace("AnyColor", "");
35    }
36    let trimmed = choices_mut.trim().trim_end_matches(',');
37    if !trimmed.is_empty() {
38        out.extend(
39            trimmed
40                .split(',')
41                .map(|c| c.trim().to_string())
42                .filter(|s| !s.is_empty()),
43        );
44    }
45    if out.is_empty() {
46        out = ["White", "Blue", "Black", "Red", "Green"]
47            .iter()
48            .map(|s| s.to_string())
49            .collect();
50    }
51    out
52}
53
54/// End-of-turn revert for Protection. Mirrors the `GameCommand.run()` in Java
55/// `ProtectEffect` that removes the granted protection keyword when the
56/// effect duration expires.
57pub fn run(game: &mut crate::game::GameState, card_id: crate::ids::CardId, keyword: &str) {
58    if game.card(card_id).zone == ZoneType::Battlefield {
59        game.card_mut(card_id).pump_keywords.remove(keyword);
60    }
61}
62
63/// `SP$ Protection` — grant protection from a quality to a permanent.
64///
65/// Mirrors Java's `ProtectEffect.java`.
66/// - `Gains$` — the protection keyword to grant (e.g. "Protection from chosen color").
67/// - `Choices$` — if present, player chooses what to protect from.
68///
69/// # Card script examples
70/// ```text
71/// A:SP$ Protection | Gains$ Protection from chosen color | Choices$ White,Blue,Black,Red,Green
72/// A:SP$ Protection | Gains$ Protection from red
73/// ```
74/// Struct form of this effect so it can participate in the
75/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
76/// `ProtectEffect` class extending `SpellAbilityEffect`.
77#[manabrew_engine_macros::spell_effect(ProtectEffect)]
78fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
79    let controller = sa.activating_player;
80
81    // Determine target
82    let target = sa
83        .target_chosen
84        .target_card
85        .or_else(|| match sa.defined_ref() {
86            Some(DefinedRef::SelfCard) => sa.source,
87            Some(DefinedRef::ParentTarget) => ctx.parent_target_card,
88            _ => sa.source,
89        });
90
91    let gains = sa.ir.gains.clone().unwrap_or_default();
92
93    // Mirrors Java ProtectEffect: `isChoice = sa.getParam("Gains").contains("Choice")`
94    // Handles both `Gains$ Choice` (Gods Willing) and `Gains$ Protection from chosen color`.
95    let is_choice = gains.contains("Choice") || gains.contains("chosen color");
96
97    let mut targets = target.into_iter().collect::<Vec<_>>();
98    targets.extend(card_util::get_radiance(ctx.game, sa).iter().copied());
99    targets.retain(|&id| ctx.game.card(id).zone == ZoneType::Battlefield);
100    targets.sort_unstable_by_key(|cid| cid.0);
101    targets.dedup();
102    if targets.is_empty() {
103        return;
104    }
105
106    if is_choice {
107        let choices = get_protection_list(sa);
108        let chosen = ctx.agents[controller.index()].choose_color(controller, &choices);
109        if let Some(color) = chosen {
110            let prot_kw = format!("Protection from {}", color.to_lowercase());
111            for card_id in targets {
112                ctx.game.card_mut(card_id).add_pump_keyword(&prot_kw);
113            }
114        }
115    } else {
116        // Static protection grant
117        for card_id in targets {
118            ctx.game.card_mut(card_id).add_pump_keyword(&gains);
119        }
120    }
121}