Skip to main content

manabrew_engine/ability/effects/
repeat_each_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{matches_valid_cards_for_sa, resolve_defined_players, EffectContext};
4use crate::spellability::{build_spell_ability, SpellAbility};
5
6/// `SP$ RepeatEach` — loop a sub-ability over cards or players.
7///
8/// Mirrors Java's `RepeatEachEffect.java`.
9///
10/// # Params
11/// - `RepeatSubAbility` — SVar name on source card for the sub-ability to resolve each iteration
12/// - `RepeatCards` — if present, iterate over matching cards (filter string)
13/// - `RepeatPlayers` — if present, iterate over matching players
14/// - `Zone` — zone to search for RepeatCards (default Battlefield)
15/// Struct form of this effect so it can participate in the
16/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
17/// `RepeatEachEffect` class extending `SpellAbilityEffect`.
18#[manabrew_engine_macros::spell_effect(RepeatEachEffect)]
19fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
20    let source_id = match sa.source {
21        Some(id) => id,
22        None => return,
23    };
24
25    let controller = sa.activating_player;
26    let use_damage_map = sa.ir.damage_map;
27    let use_change_zone_table = sa.ir.change_zone_table;
28
29    if use_damage_map {
30        ctx.game.ensure_pending_damage_maps();
31    }
32    if use_change_zone_table {
33        ctx.game.ensure_pending_change_zone_table();
34    }
35
36    // Get the sub-ability SVar name
37    let sub_svar_name = match sa.ir.repeat_sub_ability.as_deref() {
38        Some(name) => name,
39        None => return,
40    };
41
42    // Look up the sub-ability text from the source card's SVars
43    let sub_text = match ctx
44        .game
45        .card(source_id)
46        .get_s_var(sub_svar_name)
47        .map(str::to_string)
48    {
49        Some(text) => text,
50        None => return,
51    };
52
53    // Determine iteration mode: cards or players
54    if let Some(repeat_cards_filter) = sa.ir.repeat_cards_text.as_deref() {
55        let repeat_cards_selector = sa.ir.repeat_cards_selector.as_ref();
56        // Card iteration path
57        let zone = sa.ir.zone.unwrap_or(ZoneType::Battlefield);
58
59        // Collect matching cards
60        let matching: Vec<crate::ids::CardId> = {
61            let mut result = Vec::new();
62            for &pid in &ctx.game.player_order.clone() {
63                let zone_cards = ctx.game.cards_in_zone(zone, pid).to_vec();
64                for cid in zone_cards {
65                    if matches_valid_cards_for_sa(
66                        ctx.game,
67                        sa,
68                        ctx.game.card(cid),
69                        repeat_cards_selector,
70                        repeat_cards_filter,
71                    ) {
72                        result.push(cid);
73                    }
74                }
75            }
76            result
77        };
78
79        // Iterate: remember card → resolve sub-SA → un-remember
80        for card_id in matching {
81            ctx.game.card_mut(source_id).clear_remembered();
82            ctx.game.card_mut(source_id).add_remembered_card(card_id);
83
84            // Build and resolve sub-ability
85            let sub_sa = build_spell_ability(ctx.game, source_id, &sub_text, controller);
86            resolve_sub_chain(ctx, sub_sa);
87
88            if ctx.game.game_over {
89                break;
90            }
91        }
92
93        // Clean up remembered cards
94        ctx.game.card_mut(source_id).clear_remembered();
95    } else if let Some(repeat_players) = sa.ir.repeat_players.as_deref() {
96        // Player iteration path
97        let players = resolve_defined_players(repeat_players, controller, ctx.game);
98
99        for pid in players {
100            ctx.game.card_mut(source_id).clear_remembered();
101            ctx.game.card_mut(source_id).add_remembered_player(pid);
102
103            // Java `RepeatEachEffect` keeps sa.getActivatingPlayer() across
104            // iterations; pid flows in only via Remembered.
105            let sub_sa = build_spell_ability(ctx.game, source_id, &sub_text, controller);
106            resolve_sub_chain(ctx, sub_sa);
107
108            if ctx.game.game_over {
109                break;
110            }
111        }
112
113        ctx.game.card_mut(source_id).clear_remembered();
114    }
115
116    if use_damage_map {
117        // Mirror Java RepeatEach post-loop damage-map resolve.
118        let mut flush_sa = sa.clone();
119        flush_sa.damage_map = ctx.game.pending_damage_map.clone();
120        flush_sa.prevent_map = ctx.game.pending_prevent_map.clone();
121        super::damage_resolve_effect::DamageResolveEffect::resolve(ctx, &flush_sa);
122        ctx.game.clear_pending_damage_maps();
123    }
124    if use_change_zone_table {
125        if let Some(table) = ctx.game.pending_change_zone_table.clone() {
126            table.trigger_changes_zone_all(ctx.trigger_handler, ctx.game, Some(sa));
127            ctx.game.clear_pending_change_zone_table();
128        }
129    }
130}
131
132/// Walk a sub-ability chain (same pattern as charm_effect.rs).
133fn resolve_sub_chain(ctx: &mut EffectContext, initial: SpellAbility) {
134    let mut cur_opt: Option<SpellAbility> = Some(initial);
135    while let Some(cur_sa) = cur_opt {
136        super::resolve_effect(ctx, &cur_sa);
137        cur_opt = cur_sa.sub_ability.map(|b| *b);
138        if ctx.game.game_over {
139            break;
140        }
141    }
142}