Skip to main content

manabrew_engine/ability/effects/
pump_all_effect.rs

1use forge_foundation::ZoneType;
2
3use super::{matches_valid_cards_for_sa, EffectContext};
4use crate::card::perpetual::perpetual_interface::PerpetualInterface;
5use crate::card::perpetual::{perpetual_keywords, perpetual_pt_boost};
6use crate::ids::CardId;
7
8/// End-of-turn revert for PumpAll. Mirrors the `GameCommand.run()` in Java
9/// `PumpAllEffect` that reverses the P/T bonus and removes granted keywords
10/// when the effect duration expires.
11pub fn run(
12    game: &mut crate::game::GameState,
13    card_id: crate::ids::CardId,
14    att_bonus: i32,
15    def_bonus: i32,
16    keywords: &[String],
17) {
18    if game.card(card_id).zone != ZoneType::Battlefield {
19        return;
20    }
21    game.card_mut(card_id).add_pt_boost(-att_bonus, -def_bonus);
22    for kw in keywords {
23        game.card_mut(card_id).pump_keywords.remove(kw);
24    }
25}
26
27/// `SP$ PumpAll` — modify P/T of all matching permanents until end of turn (or perpetually).
28///
29/// Mirrors Java's `PumpAllEffect.java`:
30/// - `NumAtt$` / `NumDef$` specify power/toughness change (signed: "+2", "-2").
31/// - `ValidCards$` selects which permanents are affected.
32/// - `PumpZone$` specifies the zone to look in (default: Battlefield, supports Hand).
33/// - `Duration$ Perpetual` stores the bonus in `perpetual_power_modifier` /
34///   `perpetual_toughness_modifier` so it persists across zone changes.
35/// - Without `Duration$ Perpetual`, uses temporary `power_modifier` / `toughness_modifier`
36///   (zeroed at cleanup by `step_cleanup`).
37///
38/// Positive values are a pump (Giant Growth effect); negative values are a
39/// debuff (Rising Miasma -2/-2).
40///
41/// # Card script examples
42/// ```text
43/// A:SP$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +2 | NumDef$ +2
44/// A:SP$ PumpAll | ValidCards$ Creature | NumAtt$ -2 | NumDef$ -2
45/// DB$ PumpAll | PumpZone$ Hand | ValidCards$ Creature.YouOwn | NumAtt$ +1 | NumDef$ +1 | Duration$ Perpetual
46/// ```
47/// Struct form of this effect so it can participate in the
48/// `SpellAbilityEffect` trait hierarchy — mirrors Java's
49/// `PumpAllEffect` class extending `SpellAbilityEffect`.
50#[manabrew_engine_macros::spell_effect(PumpAllEffect)]
51fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
52    // parse_param strips leading '+' sign via Rust's i32::from_str which accepts it.
53    // Fall back to SVar resolution for Count$Kicked etc.
54    let att_bonus = sa
55        .ir
56        .num_att
57        .as_deref()
58        .map(|raw| super::resolve_numeric_value(ctx.game, sa, raw, 0))
59        .unwrap_or(0);
60    let def_bonus = sa
61        .ir
62        .num_def
63        .as_deref()
64        .map(|raw| super::resolve_numeric_value(ctx.game, sa, raw, 0))
65        .unwrap_or(0);
66
67    // Parse KW$ parameter for keyword grants (e.g. "KW$ Haste" or "KW$ Flying & Trample")
68    let keywords: Vec<String> = sa
69        .ir
70        .kw
71        .as_deref()
72        .map(|kw_str| {
73            kw_str
74                .split('&')
75                .map(|s| s.trim().to_string())
76                .filter(|s| !s.is_empty())
77                .collect()
78        })
79        .unwrap_or_default();
80
81    if att_bonus == 0 && def_bonus == 0 && keywords.is_empty() {
82        return;
83    }
84
85    let valid_cards = sa.ir.valid_cards_selector.as_ref();
86
87    // Determine the zone to look for cards in (default: Battlefield).
88    let pump_zone_str = sa.ir.pump_zone.as_deref().unwrap_or("Battlefield");
89    let pump_zone = match pump_zone_str {
90        s if s.eq_ignore_ascii_case("Hand") => ZoneType::Hand,
91        _ => ZoneType::Battlefield,
92    };
93
94    // Perpetual effects persist across zone changes (stored in perpetual_*_modifier).
95    let is_perpetual = sa.ir.perpetual_duration;
96    let resolve_ts = if is_perpetual {
97        Some(ctx.game.next_effect_timestamp())
98    } else {
99        None
100    };
101
102    // Pass 1 — collect matching cards in the target zone
103    let player_ids = ctx.game.player_order.clone();
104    let mut to_pump: Vec<CardId> = Vec::new();
105    for &pid in &player_ids {
106        let zone_cards = ctx.game.cards_in_zone(pump_zone, pid).to_vec();
107        for cid in zone_cards {
108            if matches_valid_cards_for_sa(ctx.game, sa, ctx.game.card(cid), valid_cards, "Creature")
109            {
110                to_pump.push(cid);
111            }
112        }
113    }
114
115    // Pass 2 — apply modifiers
116    for card_id in to_pump {
117        if ctx.game.card(card_id).zone != pump_zone {
118            continue; // already moved
119        }
120        if is_perpetual {
121            let ts = resolve_ts.expect("perpetual resolve timestamp must exist");
122            let card = ctx.game.card_mut(card_id);
123            perpetual_pt_boost::PerpetualPtBoost {
124                timestamp: ts,
125                power: att_bonus,
126                toughness: def_bonus,
127            }
128            .apply_effect(card);
129            for kw in &keywords {
130                perpetual_keywords::PerpetualKeywords {
131                    timestamp: ts,
132                    add_keywords: vec![kw.clone()],
133                    remove_keywords: Vec::new(),
134                    remove_all: false,
135                }
136                .apply_effect(card);
137            }
138        } else {
139            ctx.game
140                .card_mut(card_id)
141                .add_pt_boost(att_bonus, def_bonus);
142            for kw in &keywords {
143                ctx.game.card_mut(card_id).add_pump_keyword(kw);
144            }
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use crate::ability::spell_ability_effect::SpellAbilityEffect;
152    use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
153    use std::collections::HashMap;
154
155    use crate::ability::effects::EffectContext;
156    use crate::agent::PassAgent;
157    use crate::card::Card;
158    use crate::game::GameState;
159    use crate::ids::{CardId, PlayerId};
160    use crate::mana::ManaPool;
161    use crate::spellability::SpellAbility;
162    use crate::trigger::handler::TriggerHandler;
163
164    fn make_creature(game: &mut GameState, owner: PlayerId) -> CardId {
165        let c = Card::new(
166            CardId(0),
167            "Bear".into(),
168            owner,
169            CardTypeLine::parse("Creature - Bear"),
170            ManaCost::parse("1 G"),
171            ColorSet::GREEN,
172            Some(2),
173            Some(2),
174            vec![],
175            vec![],
176        );
177        game.create_card(c)
178    }
179
180    fn make_ctx<'a>(
181        game: &'a mut GameState,
182        agents: &'a mut Vec<Box<dyn crate::agent::PlayerAgent>>,
183        th: &'a mut TriggerHandler,
184        mp: &'a mut Vec<ManaPool>,
185        templates: &'a HashMap<String, Card>,
186        templates_variants: &'a HashMap<(String, String), usize>,
187        token_fallback: &'a HashMap<String, String>,
188        edition_dates: &'a HashMap<String, String>,
189        rng: &'a mut dyn crate::game_rng::GameRng,
190    ) -> EffectContext<'a> {
191        EffectContext {
192            game,
193            combat: None,
194            agents,
195            trigger_handler: th,
196            token_templates: templates,
197            token_art_variants: templates_variants,
198            token_fallback,
199            edition_dates,
200            mana_pools: mp,
201            parent_target_card: None,
202            rng,
203        }
204    }
205
206    #[test]
207    fn pump_all_boosts_all_creatures() {
208        let mut game = GameState::new(&["Alice", "Bob"], 20);
209        let p0 = PlayerId(0);
210        let p1 = PlayerId(1);
211        let c1 = make_creature(&mut game, p0);
212        let c2 = make_creature(&mut game, p1);
213        game.move_card(c1, ZoneType::Battlefield, p0);
214        game.move_card(c2, ZoneType::Battlefield, p1);
215
216        let sa = SpellAbility::new_simple(
217            None,
218            p0,
219            "A:SP$ PumpAll | ValidCards$ Creature | NumAtt$ +2 | NumDef$ +2",
220        );
221        let mut th = TriggerHandler::new();
222        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
223            vec![Box::new(PassAgent), Box::new(PassAgent)];
224        let mut mp = vec![ManaPool::default(), ManaPool::default()];
225        let templates = HashMap::new();
226        let templates_variants: HashMap<(String, String), usize> = HashMap::new();
227        let token_fallback: HashMap<String, String> = HashMap::new();
228        let edition_dates: HashMap<String, String> = HashMap::new();
229        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
230        let mut ctx = make_ctx(
231            &mut game,
232            &mut agents,
233            &mut th,
234            &mut mp,
235            &templates,
236            &templates_variants,
237            &token_fallback,
238            &edition_dates,
239            &mut rng_adapter,
240        );
241        super::PumpAllEffect::resolve(&mut ctx, &sa);
242
243        assert_eq!(ctx.game.card(c1).power(), 4); // 2+2
244        assert_eq!(ctx.game.card(c1).toughness(), 4);
245        assert_eq!(ctx.game.card(c2).power(), 4);
246        assert_eq!(ctx.game.card(c2).toughness(), 4);
247    }
248
249    #[test]
250    fn pump_all_debuff_reduces_pt() {
251        let mut game = GameState::new(&["Alice", "Bob"], 20);
252        let p0 = PlayerId(0);
253        let c1 = make_creature(&mut game, p0);
254        game.move_card(c1, ZoneType::Battlefield, p0);
255
256        // Rising Miasma: -2/-2 to all
257        let sa = SpellAbility::new_simple(
258            None,
259            p0,
260            "A:SP$ PumpAll | ValidCards$ Creature | NumAtt$ -2 | NumDef$ -2",
261        );
262        let mut th = TriggerHandler::new();
263        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
264            vec![Box::new(PassAgent), Box::new(PassAgent)];
265        let mut mp = vec![ManaPool::default(), ManaPool::default()];
266        let templates = HashMap::new();
267        let templates_variants: HashMap<(String, String), usize> = HashMap::new();
268        let token_fallback: HashMap<String, String> = HashMap::new();
269        let edition_dates: HashMap<String, String> = HashMap::new();
270        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
271        let mut ctx = make_ctx(
272            &mut game,
273            &mut agents,
274            &mut th,
275            &mut mp,
276            &templates,
277            &templates_variants,
278            &token_fallback,
279            &edition_dates,
280            &mut rng_adapter,
281        );
282        super::PumpAllEffect::resolve(&mut ctx, &sa);
283
284        assert_eq!(ctx.game.card(c1).power(), 0); // 2-2
285        assert_eq!(ctx.game.card(c1).toughness(), 0);
286    }
287
288    #[test]
289    fn pump_all_you_ctrl_only_affects_your_creatures() {
290        let mut game = GameState::new(&["Alice", "Bob"], 20);
291        let p0 = PlayerId(0);
292        let p1 = PlayerId(1);
293        let mine = make_creature(&mut game, p0);
294        let theirs = make_creature(&mut game, p1);
295        game.move_card(mine, ZoneType::Battlefield, p0);
296        game.move_card(theirs, ZoneType::Battlefield, p1);
297
298        // Righteous Charge: creatures you control get +2/+2
299        let sa = SpellAbility::new_simple(
300            None,
301            p0,
302            "A:SP$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +2 | NumDef$ +2",
303        );
304        let mut th = TriggerHandler::new();
305        let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
306            vec![Box::new(PassAgent), Box::new(PassAgent)];
307        let mut mp = vec![ManaPool::default(), ManaPool::default()];
308        let templates = HashMap::new();
309        let templates_variants: HashMap<(String, String), usize> = HashMap::new();
310        let token_fallback: HashMap<String, String> = HashMap::new();
311        let edition_dates: HashMap<String, String> = HashMap::new();
312        let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
313        let mut ctx = make_ctx(
314            &mut game,
315            &mut agents,
316            &mut th,
317            &mut mp,
318            &templates,
319            &templates_variants,
320            &token_fallback,
321            &edition_dates,
322            &mut rng_adapter,
323        );
324        super::PumpAllEffect::resolve(&mut ctx, &sa);
325
326        assert_eq!(ctx.game.card(mine).power(), 4); // boosted
327        assert_eq!(ctx.game.card(theirs).power(), 2); // unchanged
328    }
329}