Skip to main content

manabrew_engine/ability/effects/
effect_resolver.rs

1//! Effect resolution driver — dispatch on `ApiType` and walk sub-ability chains.
2//!
3//! Mirrors Java's `AbilityUtils.resolveApiAbility` + the target-context
4//! propagation that stack resolution performs in `SpellAbility.resolve`.
5
6use crate::ability::api_type::ApiType;
7use crate::ability::spell_ability_effect::SpellAbilityEffect;
8use crate::ids::{CardId, PlayerId};
9use crate::spellability::SpellAbility;
10
11use super::condition::{check_condition, check_condition_present};
12use super::cost_payment::resolve_effect_with_unless_cost;
13use super::effect_context::EffectContext;
14
15// Bring every sibling effect submodule into scope so the dispatch macro below
16// can reference effect types by short module path, matching Java's flat
17// package layout.
18use super::*;
19
20/// Generates effect hook dispatch from a single source of truth. Adding a new
21/// effect requires only one entry whose right-hand side names the effect's
22/// struct (which must implement `SpellAbilityEffect`).
23macro_rules! effect_dispatch {
24    ( $( $api:path => $handler:path $( [build = $build:path] )? ),* $(,)? ) => {
25        /// All API types that have implemented effect handlers.
26        /// Used by the fuzz card pool filter to exclude cards with unimplemented effects.
27        pub const IMPLEMENTED_API_TYPES: &[ApiType] = &[ $( $api ),* ];
28
29        /// Dispatch Java-parity `SpellAbilityEffect.buildSpellAbility`.
30        pub(crate) fn build_spell_ability_for_api(api_type: ApiType, sa: &mut SpellAbility) {
31            match api_type {
32                $( $api => effect_dispatch!(@build sa, $handler $(, $build)?), )*
33                _ => {}
34            }
35        }
36
37        /// Inner dispatch for a single execution of an effect. Each arm calls
38        /// the trait's associated `resolve(ctx, sa)` directly on the struct
39        /// — no instance needed (every effect is a unit struct).
40        pub(super) fn resolve_effect_once(ctx: &mut EffectContext, sa: &SpellAbility) {
41            let api_type = match sa.api {
42                Some(api) => api,
43                None => {
44                    // Some DB/SVar helper nodes intentionally have no API payload.
45                    // Java treats those as no-op leafs; avoid warning spam in parity runs.
46                    return;
47                }
48            };
49            match api_type {
50                $( $api => <$handler as SpellAbilityEffect>::resolve(ctx, sa), )*
51                _ => {
52                    let err = crate::ability::IllegalAbilityException::new(
53                        format!("Unimplemented effect API type: {:?}", api_type),
54                    );
55                    eprintln!("{}", err);
56                }
57            }
58        }
59    };
60
61    (@build $sa:ident, $handler:path, $build:path) => {
62        $build($sa)
63    };
64
65    (@build $sa:ident, $handler:path) => {
66        <$handler as SpellAbilityEffect>::build_spell_ability($sa)
67    };
68}
69
70effect_dispatch! {
71    ApiType::DealDamage => damage_deal_effect::DamageDealEffect,
72    ApiType::Branch => branch_effect::BranchEffect,
73    ApiType::GainLife => life_gain_effect::LifeGainEffect,
74    ApiType::LoseLife => life_lose_effect::LifeLoseEffect,
75    ApiType::PutCounter => counters_put_effect::CountersPutEffect,
76    ApiType::RemoveCounter => counters_remove_effect::CountersRemoveEffect,
77    ApiType::Poison => poison_effect::PoisonEffect,
78    ApiType::Pump => pump_effect::PumpEffect,
79    ApiType::Destroy => destroy_effect::DestroyEffect,
80    ApiType::Draw => draw_effect::DrawEffect,
81    ApiType::ChangeZoneAll => change_zone_all_effect::ChangeZoneAllEffect [build = change_zone_all_effect::build_spell_ability],
82    ApiType::ChangeZone => change_zone_effect::ChangeZoneEffect [build = change_zone_effect::build_spell_ability],
83    ApiType::SacrificeAll => sacrifice_all_effect::SacrificeAllEffect,
84    ApiType::Sacrifice => sacrifice_effect::SacrificeEffect,
85    ApiType::CopyPermanent => copy_permanent_effect::CopyPermanentEffect,
86    ApiType::Token => token_effect::TokenEffect,
87    ApiType::Amass => amass_effect::AmassEffect,
88    ApiType::Manifest => manifest_effect::ManifestEffect,
89    ApiType::ManifestDread => manifest_dread_effect::ManifestDreadEffect,
90    ApiType::Cloak => cloak_effect::CloakEffect,
91    ApiType::Investigate => investigate_effect::InvestigateEffect,
92    ApiType::Incubate => incubate_effect::IncubateEffect,
93    ApiType::Seek => seek_effect::SeekEffect,
94    ApiType::Learn => learn_effect::LearnEffect,
95    ApiType::Discover => discover_effect::DiscoverEffect,
96    ApiType::Meld => meld_effect::MeldEffect,
97    ApiType::ExchangeControl => control_exchange_effect::ControlExchangeEffect,
98    ApiType::ControlPlayer => control_player_effect::ControlPlayerEffect,
99    ApiType::Clash => clash_effect::ClashEffect,
100    ApiType::Vote => vote_effect::VoteEffect,
101    ApiType::VillainousChoice => villainous_choice_effect::VillainousChoiceEffect,
102    ApiType::Ascend => ascend_effect::AscendEffect,
103    ApiType::DayTime => day_time_effect::DayTimeEffect,
104    ApiType::Haunt => haunt_effect::HauntEffect,
105    ApiType::Unattach => unattach_effect::UnattachEffect,
106    ApiType::FlipOntoBattlefield => flip_onto_battlefield_effect::FlipOntoBattlefieldEffect,
107    ApiType::ClassLevelUp => class_level_up_effect::ClassLevelUpEffect,
108    ApiType::Venture => venture_effect::VentureEffect,
109    ApiType::RingTemptsYou => ring_tempts_you_effect::RingTemptsYouEffect,
110    ApiType::Heist => heist_effect::HeistEffect,
111    ApiType::ImmediateTrigger => immediate_trigger_effect::ImmediateTriggerEffect,
112    ApiType::StoreSVar => store_s_var_effect::StoreSVarEffect,
113    ApiType::ChangeTargets => change_targets_effect::ChangeTargetsEffect [build = change_targets_effect::build_spell_ability],
114    ApiType::ChangeText => change_text_effect::ChangeTextEffect,
115    ApiType::ChangeX => change_x_effect::ChangeXEffect,
116    ApiType::CountersMove => counters_move_effect::CountersMoveEffect,
117    ApiType::MultiplyCounter => counters_multiply_effect::CountersMultiplyEffect,
118    ApiType::CountersNote => counters_note_effect::CountersNoteEffect,
119    ApiType::RemoveCounterAll => counters_remove_all_effect::CountersRemoveAllEffect,
120    ApiType::ReorderZone => reorder_zone_effect::ReorderZoneEffect,
121    ApiType::Repeat => repeat_effect::RepeatEffect,
122    ApiType::ReplaceEffect => replace_effect::ReplaceEffect,
123    ApiType::BidLife => bid_life_effect::BidLifeEffect,
124    ApiType::Block => block_effect::BlockEffect,
125    ApiType::Bond => bond_effect::BondEffect,
126    ApiType::ChooseCardName => choose_card_name_effect::ChooseCardNameEffect,
127    ApiType::ChooseGeneric => choose_generic_effect::ChooseGenericEffect,
128    ApiType::ControlSpell => control_spell_effect::ControlSpellEffect [build = control_spell_effect::build_spell_ability],
129    ApiType::DamagePrevent => damage_prevent_effect::DamagePreventEffect,
130    ApiType::ExchangeLifeVariant => life_exchange_variant_effect::LifeExchangeVariantEffect,
131    ApiType::ReplaceDamage => replace_damage_effect::ReplaceDamageEffect,
132    ApiType::ReplaceMana => replace_mana_effect::ReplaceManaEffect,
133    ApiType::ReplaceCounter => replace_counter_effect::ReplaceCounterEffect,
134    ApiType::ReplaceToken => replace_token_effect::ReplaceTokenEffect,
135    ApiType::ReplaceSplitDamage => replace_split_damage_effect::ReplaceSplitDamageEffect,
136    ApiType::ExchangeTextBox => text_box_exchange_effect::TextBoxExchangeEffect,
137    ApiType::SwitchBlock => switch_block_effect::SwitchBlockEffect,
138    ApiType::ChangeCombatants => change_combatants_effect::ChangeCombatantsEffect,
139    ApiType::Mana => mana_effect::ManaEffect [build = mana_effect::build_spell_ability],
140    ApiType::ManaReflected => mana_reflected_effect::ManaReflectedEffect [build = mana_reflected_effect::build_spell_ability],
141    ApiType::Mill => mill_effect::MillEffect,
142    ApiType::Scry => scry_effect::ScryEffect,
143    ApiType::Surveil => surveil_effect::SurveilEffect,
144    ApiType::Dig => dig_effect::DigEffect,
145    ApiType::DigMultiple => dig_multiple_effect::DigMultipleEffect,
146    ApiType::RearrangeTopOfLibrary => rearrange_top_of_library_effect::RearrangeTopOfLibraryEffect,
147    ApiType::Reveal => reveal_effect::RevealEffect,
148    ApiType::RevealHand => reveal_hand_effect::RevealHandEffect,
149    ApiType::LookAt => look_at_effect::LookAtEffect,
150    ApiType::Charm => charm_effect::CharmEffect,
151    // Java `ApiType.CompanionChoose` binds to `CharmEffect.class` (ApiType.java:214).
152    ApiType::CompanionChoose => charm_effect::CharmEffect,
153    ApiType::GenericChoice => choose_generic_effect::ChooseGenericEffect,
154    ApiType::Plot => plot_effect::PlotEffect,
155    ApiType::PeekAndReveal => peek_and_reveal_effect::PeekAndRevealEffect,
156    ApiType::SetState => set_state_effect::SetStateEffect,
157    ApiType::Cleanup => cleanup_effect::CleanupEffect,
158    ApiType::Counter => counter_effect::CounterEffect [build = counter_effect::build_spell_ability],
159    ApiType::GainControl => control_gain_effect::ControlGainEffect,
160    ApiType::Fight => fight_effect::FightEffect,
161    ApiType::Discard => discard_effect::DiscardEffect,
162    ApiType::Attach => attach_effect::AttachEffect,
163    ApiType::DestroyAll => destroy_all_effect::DestroyAllEffect,
164    ApiType::DamageAll => damage_all_effect::DamageAllEffect,
165    ApiType::PumpAll => pump_all_effect::PumpAllEffect,
166    ApiType::TapAll => tap_all_effect::TapAllEffect,
167    ApiType::TapOrUntapAll => tap_or_untap_all_effect::TapOrUntapAllEffect,
168    ApiType::UntapAll => untap_all_effect::UntapAllEffect,
169    ApiType::Tap => tap_effect::TapEffect,
170    ApiType::TapOrUntap => tap_or_untap_effect::TapOrUntapEffect,
171    ApiType::Untap => untap_effect::UntapEffect,
172    ApiType::SetLife => life_set_effect::LifeSetEffect,
173    ApiType::ExchangeLife => life_exchange_effect::LifeExchangeEffect,
174    ApiType::WinsGame => game_win_effect::GameWinEffect,
175    ApiType::LosesGame => game_loss_effect::GameLossEffect,
176    ApiType::GameDrawn => game_draw_effect::GameDrawEffect,
177    ApiType::AddTurn => add_turn_effect::AddTurnEffect,
178    ApiType::ActivateAbility => activate_ability_effect::ActivateAbilityEffect,
179    ApiType::Fog => fog_effect::FogEffect,
180    ApiType::ReverseTurnOrder => reverse_turn_order_effect::ReverseTurnOrderEffect,
181    ApiType::EndCombatPhase => end_combat_phase_effect::EndCombatPhaseEffect,
182    ApiType::EndTurn => end_turn_effect::EndTurnEffect,
183    ApiType::ExchangePower => power_exchange_effect::PowerExchangeEffect,
184    ApiType::BecomeMonarch => become_monarch_effect::BecomeMonarchEffect,
185    ApiType::TakeInitiative => take_initiative_effect::TakeInitiativeEffect,
186    ApiType::SkipTurn => skip_turn_effect::SkipTurnEffect,
187    ApiType::SkipPhase => skip_phase_effect::SkipPhaseEffect,
188    ApiType::AddPhase => add_phase_effect::AddPhaseEffect,
189    ApiType::Phases => phases_effect::PhasesEffect,
190    ApiType::Regenerate => regenerate_effect::RegenerateEffect,
191    ApiType::Play => play_effect::PlayEffect,
192    ApiType::Animate => animate_effect::AnimateEffect,
193    ApiType::AnimateAll => animate_all_effect::AnimateAllEffect,
194    ApiType::Balance => balance_effect::BalanceEffect,
195    ApiType::ChooseCard => choose_card_effect::ChooseCardEffect,
196    ApiType::ChooseColor => choose_color_effect::ChooseColorEffect,
197    ApiType::ChooseDirection => choose_direction_effect::ChooseDirectionEffect,
198    ApiType::ChooseEvenOdd => choose_even_odd_effect::ChooseEvenOddEffect,
199    ApiType::Clone => clone_effect::CloneEffect,
200    ApiType::Connive => connive_effect::ConniveEffect,
201    ApiType::GainControlVariant => control_gain_variant_effect::ControlGainVariantEffect,
202    ApiType::RepeatEach => repeat_each_effect::RepeatEachEffect,
203    ApiType::Shuffle => shuffle_effect::ShuffleEffect,
204    ApiType::PutCounterAll => counters_put_all_effect::CountersPutAllEffect,
205    ApiType::AddOrRemoveCounter => counters_put_or_remove_effect::CountersPutOrRemoveEffect,
206    ApiType::EachDamage => damage_each_effect::DamageEachEffect,
207    ApiType::Effect => effect_effect::EffectEffect,
208    ApiType::DelayedTrigger => delayed_trigger_effect::DelayedTriggerEffect,
209    ApiType::DrainMana => drain_mana_effect::DrainManaEffect,
210    ApiType::RemoveFromCombat => remove_from_combat_effect::RemoveFromCombatEffect,
211    ApiType::Detain => detain_effect::DetainEffect,
212    ApiType::Goad => goad_effect::GoadEffect,
213    ApiType::ChoosePlayer => choose_player_effect::ChoosePlayerEffect,
214    ApiType::ChooseSource => choose_source_effect::ChooseSourceEffect,
215    ApiType::ChooseType => choose_type_effect::ChooseTypeEffect,
216    ApiType::NameCard => name_card_effect::NameCardEffect,
217    ApiType::ChooseNumber => choose_number_effect::ChooseNumberEffect,
218    ApiType::DigUntil => dig_until_effect::DigUntilEffect,
219    ApiType::FlipACoin => flip_coin_effect::FlipCoinEffect,
220    ApiType::Explore => explore_effect::ExploreEffect,
221    ApiType::RollDice => roll_dice_effect::RollDiceEffect,
222    ApiType::Protection => protect_effect::ProtectEffect,
223    ApiType::ProtectionAll => protect_all_effect::ProtectAllEffect,
224    ApiType::PreventDamage => prevent_damage_effect::PreventDamageEffect,
225    ApiType::Proliferate => counters_proliferate_effect::CountersProliferateEffect,
226    ApiType::MoveCounter => move_counter_effect::MoveCounterEffect,
227    ApiType::TimeTravel => time_travel_effect::TimeTravelEffect,
228    ApiType::MustBlock => must_block_effect::MustBlockEffect,
229    ApiType::CopySpellAbility => copy_spell_ability_effect::CopySpellAbilityEffect [build = copy_spell_ability_effect::build_spell_ability],
230    ApiType::TwoPiles => two_piles_effect::TwoPilesEffect,
231    ApiType::Encode => encode_effect::EncodeEffect,
232
233    // ── Aliases: variants whose script names map via smart_value_of ────
234    ApiType::ExchangeControlVariant => control_gain_variant_effect::ControlGainVariantEffect,
235    ApiType::ExchangeZone => change_zone_effect::ChangeZoneEffect,
236    ApiType::Regeneration => regenerate_effect::RegenerateEffect,
237
238    // ── Niche/format-specific effects ─────────────────────────────────
239    ApiType::Abandon => abandon_effect::AbandonEffect,
240    ApiType::AdvanceCrank => advance_crank_effect::AdvanceCrankEffect,
241    ApiType::Airbend => airbend_effect::AirbendEffect,
242    ApiType::AlterAttribute => alter_attribute_effect::AlterAttributeEffect,
243    ApiType::AssembleContraption => assemble_contraption_effect::AssembleContraptionEffect,
244    ApiType::AssignGroup => assign_group_effect::AssignGroupEffect,
245    ApiType::BecomesBlocked => becomes_blocked_effect::BecomesBlockedEffect,
246    ApiType::BlankLine => blank_line_effect::BlankLineEffect,
247    ApiType::Blight => blight_effect::BlightEffect,
248    ApiType::Camouflage => camouflage_effect::CamouflageEffect,
249    ApiType::ChangeSpeed => change_speed_effect::ChangeSpeedEffect,
250    ApiType::ChaosEnsues => chaos_ensues_effect::ChaosEnsuesEffect,
251    ApiType::ChooseSector => choose_sector_effect::ChooseSectorEffect,
252    ApiType::ClaimThePrize => claim_the_prize_effect::ClaimThePrizeEffect,
253    ApiType::DamageResolve => damage_resolve_effect::DamageResolveEffect,
254    ApiType::Debuff => debuff_effect::DebuffEffect,
255    ApiType::Draft => draft_effect::DraftEffect,
256    ApiType::Earthbend => earthbend_effect::EarthbendEffect [build = earthbend_effect::build_spell_ability],
257    ApiType::Endure => endure_effect::EndureEffect,
258    ApiType::GainOwnership => ownership_gain_effect::OwnershipGainEffect,
259    ApiType::Intensify => intensify_effect::IntensifyEffect,
260    ApiType::LosePerpetual => lose_perpetual_effect::LosePerpetualEffect,
261    ApiType::MakeCard => make_card_effect::MakeCardEffect,
262    ApiType::MultiplePiles => multiple_piles_effect::MultiplePilesEffect,
263    ApiType::Mutate => mutate_effect::MutateEffect,
264    ApiType::OpenAttraction => open_attraction_effect::OpenAttractionEffect,
265    ApiType::PermanentCreature => permanent_creature_effect::PermanentCreatureEffect,
266    ApiType::PermanentNoncreature => permanent_noncreature_effect::PermanentNoncreatureEffect,
267    ApiType::Planeswalk => planeswalk_effect::PlaneswalkEffect,
268    ApiType::Radiation => radiation_effect::RadiationEffect,
269    ApiType::InternalRadiation => internal_radiation_effect::InternalRadiationEffect,
270    ApiType::ZoneExchange => zone_exchange_effect::ZoneExchangeEffect,
271    ApiType::RemoveFromGame => remove_from_game_effect::RemoveFromGameEffect,
272    ApiType::RemoveFromMatch => remove_from_match_effect::RemoveFromMatchEffect,
273    ApiType::RestartGame => restart_game_effect::RestartGameEffect,
274    ApiType::RollPlanarDice => roll_planar_dice_effect::RollPlanarDiceEffect,
275    ApiType::RunChaos => run_chaos_effect::RunChaosEffect,
276    ApiType::SetInMotion => set_in_motion_effect::SetInMotionEffect,
277    ApiType::Subgame => subgame_effect::SubgameEffect,
278    ApiType::UnlockDoor => unlock_door_effect::UnlockDoorEffect,
279    ApiType::ChangeZoneResolve => change_zone_resolve_effect::ChangeZoneResolveEffect,
280    ApiType::PlayLandVariant => play_land_variant_effect::PlayLandVariantEffect,
281}
282
283/// Resolve a single SpellAbility node's effect by dispatching on its API type.
284/// Mirrors Java's `AbilityUtils.resolveApiAbility(sa)`.
285pub fn resolve_effect(ctx: &mut EffectContext, sa: &SpellAbility) {
286    // Check condition gate (e.g. Kicked) — skip this effect if condition not met
287    if !check_condition(ctx.game, sa) {
288        return;
289    }
290
291    // Check ConditionPresent$ / ConditionZone$ / ConditionCompare$ conditions
292    let source_id = match sa.source {
293        Some(id) => id,
294        None => return, // No source card — skip condition check
295    };
296    if !check_condition_present(ctx.game, sa, sa.activating_player, source_id) {
297        return;
298    }
299
300    // Handle Repeat$ — repeat the effect N times (for Multikicker/Replicate-like scaling).
301    // Mirrors Java's AbilityUtils.handleRepeatParam().
302    let repeat_count = if let Some(repeat_val) = sa.ir.repeat.as_deref() {
303        match repeat_val {
304            "KickerNum" => sa.kick_count as i32,
305            _ => 1,
306        }
307    } else {
308        1
309    };
310
311    for _ in 0..repeat_count {
312        if let Some(unless_cost) = sa
313            .ir
314            .unless_cost
315            .as_deref()
316            .map(|s: &str| s.trim())
317            .filter(|s| !s.is_empty())
318        {
319            resolve_effect_with_unless_cost(ctx, sa, unless_cost);
320        } else {
321            resolve_effect_once(ctx, sa);
322        }
323    }
324}
325
326/// Resolve a SpellAbility and its linked sub-ability chain, inheriting parent
327/// card/player targets onto child nodes when those nodes don't choose their own.
328/// Mirrors the target-context propagation used by stack resolution.
329pub fn resolve_effect_chain_with_parent(
330    ctx: &mut EffectContext,
331    initial: SpellAbility,
332    initial_parent_target_card: Option<CardId>,
333    initial_parent_target_player: Option<PlayerId>,
334) {
335    let mut current = Some(initial);
336    let mut parent_target_card = initial_parent_target_card;
337    let mut parent_target_player = initial_parent_target_player;
338    let mut is_first = true;
339
340    while let Some(sa) = current {
341        let mut sa_with_ctx;
342        let needs_ctx_clone = !is_first
343            && ((parent_target_card.is_some() && sa.target_chosen.target_card.is_none())
344                || (parent_target_player.is_some() && sa.target_chosen.target_player.is_none()));
345        let sa_ref = if needs_ctx_clone {
346            sa_with_ctx = sa.clone();
347            if sa_with_ctx.target_chosen.target_card.is_none() {
348                sa_with_ctx.target_chosen.target_card = parent_target_card;
349            }
350            if sa_with_ctx.target_chosen.target_player.is_none() {
351                sa_with_ctx.target_chosen.target_player = parent_target_player;
352            }
353            &sa_with_ctx
354        } else {
355            &sa
356        };
357
358        resolve_effect(ctx, sa_ref);
359        parent_target_card = sa_ref.target_chosen.target_card.or(parent_target_card);
360        parent_target_player = sa_ref.target_chosen.target_player.or(parent_target_player);
361        current = if sub_ability_handled_internally(sa_ref) {
362            None
363        } else {
364            sa.sub_ability.map(|b| *b)
365        };
366        is_first = false;
367        if ctx.game.game_over {
368            break;
369        }
370    }
371}
372
373pub fn resolve_effect_chain(ctx: &mut EffectContext, initial: SpellAbility) {
374    resolve_effect_chain_with_parent(ctx, initial, None, None);
375}
376
377pub(crate) fn sub_ability_handled_internally(sa: &SpellAbility) -> bool {
378    sa.ir.unless_cost.is_some()
379}