manabrew_engine/ability/effects/
pump_all_effect.rs1use 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
8pub 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#[manabrew_engine_macros::spell_effect(PumpAllEffect)]
51fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
52 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 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 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 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 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 for card_id in to_pump {
117 if ctx.game.card(card_id).zone != pump_zone {
118 continue; }
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); 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 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); 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 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); assert_eq!(ctx.game.card(theirs).power(), 2); }
329}