Skip to main content

manabrew_engine/ability/effects/
counters_proliferate_effect.rs

1use forge_foundation::ZoneType;
2
3use super::EffectContext;
4use crate::agent::GameEntity;
5use crate::card::CounterType;
6use crate::event::RunParams;
7use crate::ids::CardId;
8use crate::replacement::replacement_handler::{apply_replacements_with_agents, ReplacementEvent};
9use crate::trigger::TriggerType;
10
11/// `SP$ Proliferate` — choose any number of permanents and/or players that
12/// have counters on them, then add one counter of each kind already there.
13///
14/// Mirrors Java's `CountersProliferateEffect.java`.
15///
16/// # Card script examples
17/// ```text
18/// A:SP$ Proliferate
19/// ```
20/// Struct form of this effect so it can participate in the
21/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
22/// `CountersProliferateEffect` class extending `SpellAbilityEffect`.
23#[manabrew_engine_macros::spell_effect(CountersProliferateEffect)]
24fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
25    let controller = sa.activating_player;
26
27    // Run Proliferate replacement effects before proliferating.
28    let mut prolif_event = ReplacementEvent::Proliferate {
29        player: controller,
30        count: 1,
31    };
32    let prolif_result =
33        apply_replacements_with_agents(&mut *ctx.game, ctx.agents, &mut prolif_event);
34    if prolif_result == crate::replacement::ReplacementResult::Skipped
35        || prolif_result == crate::replacement::ReplacementResult::Replaced
36    {
37        return;
38    }
39
40    // Build unified candidate list: players first, then permanents (matches Java order).
41    // Java: list.addAll(game.getPlayers().filter(PlayerPredicates.hasCounters()));
42    //       list.addAll(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.hasCounters()));
43    let player_ids = ctx.game.player_order.clone();
44    let mut candidates: Vec<GameEntity> = Vec::new();
45
46    // Players with counters (poison, energy, etc.)
47    for &pid in &player_ids {
48        let p = ctx.game.player(pid);
49        if p.poison_counters > 0 || p.energy_counters > 0 {
50            candidates.push(GameEntity::Player(pid));
51        }
52    }
53
54    // Battlefield permanents with counters
55    for &pid in &player_ids {
56        let zone_cards = ctx.game.cards_in_zone(ZoneType::Battlefield, pid).to_vec();
57        for cid in zone_cards {
58            if !ctx.game.card(cid).counters.is_empty()
59                && ctx.game.card(cid).counters.values().any(|&v| v > 0)
60            {
61                candidates.push(GameEntity::Card(cid));
62            }
63        }
64    }
65
66    if candidates.is_empty() {
67        return;
68    }
69
70    // Player chooses which entities to proliferate (0..all)
71    let chosen = ctx.agents[controller.index()].choose_entities_for_effect(
72        controller,
73        &candidates,
74        0,
75        candidates.len(),
76    );
77
78    // Add one counter of each type already present on chosen entities
79    for entity in chosen {
80        match entity {
81            GameEntity::Player(pid) => {
82                proliferate_player(ctx, pid);
83            }
84            GameEntity::Card(cid) => {
85                proliferate_card(ctx, cid);
86            }
87        }
88    }
89}
90
91/// Add one of each counter type already on a player.
92fn proliferate_player(ctx: &mut EffectContext, pid: crate::ids::PlayerId) {
93    let p = ctx.game.player(pid);
94    if p.poison_counters > 0 {
95        ctx.game.player_add_poison(pid, 1);
96    }
97    let p = ctx.game.player(pid);
98    if p.energy_counters > 0 {
99        ctx.game.player_add_energy(pid, 1);
100    }
101}
102
103/// Add one of each counter type already on a permanent.
104fn proliferate_card(ctx: &mut EffectContext, cid: CardId) {
105    if ctx.game.card(cid).zone != ZoneType::Battlefield {
106        return;
107    }
108    // Snapshot existing counter types
109    let counter_types: Vec<CounterType> = ctx
110        .game
111        .card(cid)
112        .counters
113        .iter()
114        .filter(|(_, &count)| count > 0)
115        .map(|(ct, _)| ct.clone())
116        .collect();
117
118    for ct in &counter_types {
119        if crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
120            &ctx.game.cards,
121            ctx.game.card(cid),
122            ct,
123        ) {
124            continue;
125        }
126        if let Some(max) = crate::staticability::static_ability_max_counter::max_counter(
127            &ctx.game.cards,
128            ctx.game.card(cid),
129            ct,
130        ) {
131            if ctx.game.card(cid).counter_count(ct) >= max {
132                continue;
133            }
134        }
135        // Run AddCounter replacement effects.
136        let mut add_event = ReplacementEvent::AddCounter {
137            target: cid,
138            counter_type: ct.clone(),
139            count: 1,
140            is_effect: true,
141        };
142        apply_replacements_with_agents(&mut *ctx.game, ctx.agents, &mut add_event);
143        let final_count = if let ReplacementEvent::AddCounter { count, .. } = add_event {
144            count
145        } else {
146            1
147        };
148        ctx.game.card_mut(cid).add_counter(ct, final_count);
149        ctx.trigger_handler.run_trigger(
150            TriggerType::CounterAdded,
151            RunParams {
152                card: Some(cid),
153                counter_type: Some(format!("{:?}", ct)),
154                counter_amount: Some(1),
155                ..Default::default()
156            },
157            false,
158        );
159    }
160}