Skip to main content

manabrew_engine/ability/effects/
choose_generic_effect.rs

1//! ChooseGeneric effect — generic modal choice.
2
3use super::EffectContext;
4use crate::ability::spell_ability_effect::get_defined_players_or_targeted;
5use crate::spellability::{build_spell_ability, SpellAbility};
6
7/// Struct form of this effect so it can participate in the
8/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
9/// `ChooseGenericEffect` class extending `SpellAbilityEffect`.
10#[manabrew_engine_macros::spell_effect(ChooseGenericEffect)]
11fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
12    let source_id = match sa.source {
13        Some(id) => id,
14        None => return,
15    };
16
17    let player = sa.activating_player;
18
19    // Collect SVar names for each choice — mirrors Java's
20    // sa.getAdditionalAbilityList("Choices")
21    let choices_str = sa.ir.choices.clone().unwrap_or_default();
22    if choices_str.is_empty() {
23        return;
24    }
25    let choice_svars: Vec<&str> = choices_str.split(',').map(|s| s.trim()).collect();
26
27    // Resolve SVar names to ability text from the source card
28    let svars = ctx.game.card(source_id).svars.clone();
29    let choice_texts: Vec<String> = choice_svars
30        .iter()
31        .filter_map(|svar| svars.get(*svar).cloned())
32        .collect();
33
34    if choice_texts.is_empty() {
35        return;
36    }
37
38    let amount = sa
39        .ir
40        .amount
41        .as_deref()
42        .and_then(|s| s.parse::<usize>().ok())
43        .unwrap_or(1);
44    let temp_remember = sa.ir.temp_remember;
45    let choosers = get_defined_players_or_targeted(ctx.game, sa);
46    for chooser in choosers {
47        let mut abilities: Vec<SpellAbility> = choice_texts
48            .iter()
49            .map(|text| {
50                let mut choice_sa = build_spell_ability(ctx.game, source_id, text, player);
51                choice_sa.source = Some(source_id);
52                choice_sa.trigger_remembered_amount = sa.trigger_remembered_amount;
53                choice_sa
54            })
55            .collect();
56
57        if let Some(n_str) = sa.ir.num_random_choices.as_deref() {
58            let n = crate::ability::ability_utils::calculate_amount(n_str) as usize;
59            while abilities.len() > n {
60                let idx = ctx.rng.next_int(abilities.len() as i32) as usize;
61                abilities.remove(idx);
62            }
63        }
64
65        abilities.retain(|choice_sa| {
66            if let Some(unless_cost_str) = choice_sa.ir.unless_cost.as_deref() {
67                let cost = crate::cost::parse_cost(unless_cost_str);
68                crate::cost::can_pay_with_ability(
69                    &cost,
70                    ctx.game,
71                    &ctx.mana_pools[chooser.index()],
72                    source_id,
73                    chooser,
74                    Some(choice_sa),
75                )
76            } else {
77                true
78            }
79        });
80
81        let mut chosen_sas: Vec<SpellAbility>;
82
83        if sa.ir.at_random {
84            chosen_sas = Vec::new();
85            for _ in 0..amount.min(abilities.len()) {
86                if abilities.is_empty() {
87                    break;
88                }
89                let idx = ctx.rng.next_int(abilities.len() as i32) as usize;
90                chosen_sas.push(abilities.remove(idx));
91            }
92
93            if sa.ir.at_random_text.as_deref() == Some("Urza") {
94                let mut i = 0;
95                while i < chosen_sas.len() {
96                    if !chosen_sas[i].uses_targeting() {
97                        i += 1;
98                    } else if chosen_sas[i]
99                        .target_restrictions
100                        .as_ref()
101                        .map(|tr| tr.has_candidates(ctx.game, chooser, Some(source_id)))
102                        .unwrap_or(false)
103                    {
104                        ctx.agents[chooser.index()].choose_targets_for(
105                            &mut chosen_sas[i],
106                            ctx.game,
107                            ctx.mana_pools,
108                        );
109                        i += 1;
110                    } else if !abilities.is_empty() {
111                        let idx = ctx.rng.next_int(abilities.len() as i32) as usize;
112                        chosen_sas[i] = abilities.remove(idx);
113                    } else {
114                        i += 1;
115                    }
116                }
117            }
118        } else if !abilities.is_empty() {
119            ctx.agents[chooser.index()].snapshot_state(ctx.game, ctx.mana_pools);
120            let chosen_indices = ctx.agents[chooser.index()]
121                .choose_spell_abilities_for_effect(chooser, &abilities, amount);
122
123            chosen_sas = chosen_indices
124                .into_iter()
125                .filter_map(|i| abilities.get(i).cloned())
126                .collect();
127        } else {
128            chosen_sas = Vec::new();
129        }
130
131        // Mirrors Java's ChooseGenericEffect: TempRemember$ Chooser swaps
132        // host.remembered_players with [chooser] for the duration of the
133        // chosen sub-ability's resolution, then restores. Sub-SVars like
134        // "Defined$ Remembered" / "UnlessPayer$ Remembered" then correctly
135        // resolve to the opponent who made the choice.
136        let prior_remembered_players = if temp_remember {
137            let prior = ctx.game.card(source_id).remembered_players.clone();
138            let card = ctx.game.card_mut(source_id);
139            card.remembered_players.clear();
140            card.add_remembered_player(chooser);
141            Some(prior)
142        } else {
143            None
144        };
145
146        if !chosen_sas.is_empty() {
147            for chosen_sa in chosen_sas {
148                super::resolve_effect_chain_with_parent(
149                    ctx,
150                    chosen_sa,
151                    sa.target_chosen.target_card,
152                    sa.target_chosen.target_player,
153                );
154                if ctx.game.game_over {
155                    break;
156                }
157            }
158        } else if let Some(fallback_name) = sa.ir.fallback_ability.as_deref() {
159            if let Some(fallback_text) = svars.get(fallback_name) {
160                let mut fallback_sa =
161                    build_spell_ability(ctx.game, source_id, fallback_text, player);
162                fallback_sa.source = Some(source_id);
163                super::resolve_effect(ctx, &fallback_sa);
164            }
165        }
166
167        if let Some(prior) = prior_remembered_players {
168            let card = ctx.game.card_mut(source_id);
169            card.remembered_players = prior;
170        }
171
172        if ctx.game.game_over {
173            break;
174        }
175    }
176}