1use forge_foundation::ZoneType;
2
3use super::EffectContext;
4use crate::ability::ability_ir::DefinedRef;
5use crate::card::card_util;
6use crate::card::perpetual::perpetual_interface::PerpetualInterface;
7use crate::card::perpetual::{perpetual_keywords, perpetual_pt_boost};
8
9#[derive(Clone, Copy)]
12enum PtBonus {
13 Fixed(i32),
14 Double,
15 Triple,
16}
17
18impl PtBonus {
19 fn parse(raw: Option<&str>, fallback: impl FnOnce() -> i32) -> Self {
20 match raw {
21 Some("Double") => PtBonus::Double,
22 Some("Triple") => PtBonus::Triple,
23 _ => PtBonus::Fixed(fallback()),
24 }
25 }
26
27 fn resolve(self, current: i32) -> i32 {
29 match self {
30 PtBonus::Fixed(n) => n,
31 PtBonus::Double => current,
32 PtBonus::Triple => current * 2,
33 }
34 }
35}
36
37pub fn run(
41 game: &mut crate::game::GameState,
42 card_id: crate::ids::CardId,
43 att_bonus: i32,
44 def_bonus: i32,
45 keywords: &[String],
46) {
47 if game.card(card_id).zone != ZoneType::Battlefield {
48 return;
49 }
50 game.card_mut(card_id).power_modifier -= att_bonus;
51 game.card_mut(card_id).toughness_modifier -= def_bonus;
52 for kw in keywords {
53 game.card_mut(card_id).pump_keywords.remove(kw);
54 }
55}
56
57#[manabrew_engine_macros::spell_effect(PumpEffect)]
61fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
62 let mut pumped_targets: Vec<crate::ids::CardId> = Vec::new();
63
64 if sa.ir.optional_present {
66 let _card_name = sa.source.map(|cid| ctx.game.card(cid).card_name.clone());
67 let prompt = sa
68 .ir
69 .option_question
70 .as_deref()
71 .unwrap_or("Apply pump to target?");
72 let activator = sa.activating_player;
73 if !ctx.agents[activator.index()].confirm_action(
74 activator,
75 Some("OptionalPump"),
76 prompt,
77 &[],
78 sa.source,
79 sa.api,
80 ) {
81 return;
82 }
83 }
84
85 let att_bonus = PtBonus::parse(sa.ir.num_att.as_deref(), || {
86 match sa.ir.num_att.as_deref() {
87 Some(raw) => super::resolve_numeric_value(ctx.game, sa, raw, 0),
88 None => 0,
89 }
90 });
91 let def_bonus = PtBonus::parse(sa.ir.num_def.as_deref(), || {
92 match sa.ir.num_def.as_deref() {
93 Some(raw) => super::resolve_numeric_value(ctx.game, sa, raw, 0),
94 None => 0,
95 }
96 });
97
98 let mut keywords: Vec<String> = sa
100 .ir
101 .kw
102 .as_deref()
103 .map(|kw_str| {
104 kw_str
105 .split('&')
106 .map(|s| s.trim().to_string())
107 .filter(|s| !s.is_empty())
108 .collect()
109 })
110 .unwrap_or_default();
111
112 if let Some(kw_choice) = sa.ir.kw_choice.as_deref() {
116 let options: Vec<String> = kw_choice
117 .split(',')
118 .map(|s| s.trim().to_string())
119 .filter(|s| !s.is_empty())
120 .collect();
121 if !options.is_empty() {
122 let activator = sa.activating_player;
123 let picks =
124 ctx.agents[activator.index()].choose_mode(activator, &options, 1, 1, sa.source);
125 if let Some(&idx) = picks.first() {
126 if let Some(kw) = options.get(idx) {
127 keywords.push(kw.clone());
128 }
129 }
130 }
131 }
132
133 if sa.ir.can_block_any {
139 keywords.push("CanBlockAny".to_string());
140 }
141 if let Some(amt) = sa.ir.can_block_amount.as_deref() {
142 keywords.push(format!("CanBlock:{amt}"));
143 }
144
145 let is_perpetual = sa.ir.perpetual_duration;
146 let resolve_ts = if is_perpetual {
147 Some(ctx.game.next_effect_timestamp())
148 } else {
149 None
150 };
151
152 if sa.overloaded {
154 let valid_tgts = sa.ir.valid_tgts_text.clone().unwrap_or_default();
155 let valid_tgts_selector = sa.ir.valid_tgts_selector.as_ref();
156 let all_bf: Vec<crate::ids::CardId> = ctx
157 .game
158 .player_order
159 .clone()
160 .iter()
161 .flat_map(|&pid| ctx.game.cards_in_zone(ZoneType::Battlefield, pid).to_vec())
162 .collect();
163 for cid in all_bf {
164 if ctx.game.card(cid).zone != ZoneType::Battlefield {
165 continue;
166 }
167 if !super::matches_valid_cards_for_sa(
168 ctx.game,
169 sa,
170 ctx.game.card(cid),
171 valid_tgts_selector,
172 &valid_tgts,
173 ) {
174 continue;
175 }
176 let target = ctx.game.card(cid);
177 let att = att_bonus.resolve(target.power());
178 let def = def_bonus.resolve(target.toughness());
179 apply_pump_to_card(ctx, cid, att, def, &keywords, is_perpetual, resolve_ts);
180 }
181 return;
182 }
183
184 let mut targets = crate::ability::spell_ability_effect::get_target_cards(ctx.game, sa);
185 if targets.is_empty() && matches!(sa.defined_ref(), Some(DefinedRef::ParentTarget)) {
186 targets.extend(ctx.parent_target_card);
187 }
188 targets.extend(card_util::get_radiance(ctx.game, sa).iter().copied());
189 targets.sort_unstable_by_key(|cid| cid.0);
190 targets.dedup();
191
192 let tgt_zones = sa
193 .target_restrictions
194 .as_ref()
195 .map(|tr| tr.tgt_zone.clone())
196 .filter(|zones| !zones.is_empty())
197 .unwrap_or_else(|| vec![ZoneType::Battlefield]);
198 for target_card in targets {
199 if !tgt_zones.contains(&ctx.game.card(target_card).zone) {
200 continue;
201 }
202 let target = ctx.game.card(target_card);
203 let att = att_bonus.resolve(target.power());
204 let def = def_bonus.resolve(target.toughness());
205 apply_pump_to_card(
206 ctx,
207 target_card,
208 att,
209 def,
210 &keywords,
211 is_perpetual,
212 resolve_ts,
213 );
214 pumped_targets.push(target_card);
215 }
216
217 if pumped_targets.is_empty() && !keywords.is_empty() {
218 let pumped_players: Vec<crate::ids::PlayerId> =
219 if let Some(tp) = sa.target_chosen.target_player {
220 vec![tp]
221 } else if let Some(d) = sa.defined() {
222 crate::ability::ability_utils::resolve_defined_players_with_sa(
223 d,
224 sa,
225 sa.activating_player,
226 ctx.game,
227 )
228 } else {
229 Vec::new()
230 };
231 for player in pumped_players {
232 for kw in &keywords {
233 crate::player::add_pump_keyword_with_duration(
234 ctx.game,
235 player,
236 kw.clone(),
237 sa.ir.duration.as_ref(),
238 );
239 }
240 }
241 }
242
243 if let Some(action) = sa.ir.at_eot.as_deref() {
246 crate::ability::spell_ability_effect::register_at_eot(
247 ctx.trigger_handler,
248 ctx.game,
249 sa,
250 action,
251 pumped_targets,
252 );
253 }
254}
255
256fn apply_pump_to_card(
257 ctx: &mut EffectContext,
258 card_id: crate::ids::CardId,
259 att: i32,
260 def: i32,
261 keywords: &[String],
262 is_perpetual: bool,
263 resolve_ts: Option<i64>,
264) {
265 if is_perpetual {
266 let ts = resolve_ts.expect("perpetual resolve timestamp must exist");
267 let card = ctx.game.card_mut(card_id);
268 perpetual_pt_boost::PerpetualPtBoost {
269 timestamp: ts,
270 power: att,
271 toughness: def,
272 }
273 .apply_effect(card);
274 for kw in keywords {
275 perpetual_keywords::PerpetualKeywords {
276 timestamp: ts,
277 add_keywords: vec![kw.clone()],
278 remove_keywords: Vec::new(),
279 remove_all: false,
280 }
281 .apply_effect(card);
282 }
283 } else {
284 ctx.game.card_mut(card_id).add_pt_boost(att, def);
285 for kw in keywords {
286 ctx.game.card_mut(card_id).add_pump_keyword(kw);
287 }
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use crate::ability::spell_ability_effect::SpellAbilityEffect;
294 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
295 use std::collections::HashMap;
296
297 use crate::ability::effects::EffectContext;
298 use crate::agent::PassAgent;
299 use crate::card::Card;
300 use crate::game::GameState;
301 use crate::ids::{CardId, PlayerId};
302 use crate::mana::ManaPool;
303 use crate::spellability::SpellAbility;
304 use crate::trigger::handler::TriggerHandler;
305
306 fn make_creature(game: &mut GameState, owner: PlayerId, name: &str) -> CardId {
307 let c = Card::new(
308 CardId(0),
309 name.into(),
310 owner,
311 CardTypeLine::parse("Creature - Human Soldier"),
312 ManaCost::parse("1 W"),
313 ColorSet::WHITE,
314 Some(2),
315 Some(2),
316 vec![],
317 vec![],
318 );
319 game.create_card(c)
320 }
321
322 fn make_ctx<'a>(
323 game: &'a mut GameState,
324 agents: &'a mut Vec<Box<dyn crate::agent::PlayerAgent>>,
325 th: &'a mut TriggerHandler,
326 mp: &'a mut Vec<ManaPool>,
327 templates: &'a HashMap<String, Card>,
328 templates_variants: &'a HashMap<(String, String), usize>,
329 token_fallback: &'a HashMap<String, String>,
330 edition_dates: &'a HashMap<String, String>,
331 rng: &'a mut dyn crate::game_rng::GameRng,
332 ) -> EffectContext<'a> {
333 EffectContext {
334 game,
335 combat: None,
336 agents,
337 trigger_handler: th,
338 token_templates: templates,
339 token_art_variants: templates_variants,
340 token_fallback,
341 edition_dates,
342 mana_pools: mp,
343 parent_target_card: None,
344 rng,
345 }
346 }
347
348 #[test]
349 fn non_targeted_pump_defaults_to_self_like_java() {
350 let mut game = GameState::new(&["Alice", "Bob"], 20);
351 let p0 = PlayerId(0);
352 let guardian = make_creature(&mut game, p0, "Guardian of New Benalia");
353 game.move_card(guardian, ZoneType::Battlefield, p0);
354
355 let sa = SpellAbility::new_simple(
356 Some(guardian),
357 p0,
358 "AB$ Pump | KW$ Indestructible | SpellDescription$ CARDNAME gains indestructible until end of turn.",
359 );
360
361 let mut th = TriggerHandler::new();
362 let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
363 vec![Box::new(PassAgent), Box::new(PassAgent)];
364 let mut mp = vec![ManaPool::default(), ManaPool::default()];
365 let templates = HashMap::new();
366 let templates_variants: HashMap<(String, String), usize> = HashMap::new();
367 let token_fallback: HashMap<String, String> = HashMap::new();
368 let edition_dates: HashMap<String, String> = HashMap::new();
369 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
370 let mut ctx = make_ctx(
371 &mut game,
372 &mut agents,
373 &mut th,
374 &mut mp,
375 &templates,
376 &templates_variants,
377 &token_fallback,
378 &edition_dates,
379 &mut rng_adapter,
380 );
381
382 super::PumpEffect::resolve(&mut ctx, &sa);
383
384 assert!(ctx.game.card(guardian).has_indestructible());
385 }
386
387 #[test]
388 fn targeted_pump_does_not_fall_back_to_source() {
389 let mut game = GameState::new(&["Alice", "Bob"], 20);
390 let p0 = PlayerId(0);
391 let source = make_creature(&mut game, p0, "Source");
392 game.move_card(source, ZoneType::Battlefield, p0);
393
394 let sa = SpellAbility::new_simple(
395 Some(source),
396 p0,
397 "SP$ Pump | ValidTgts$ Creature | KW$ Indestructible",
398 );
399
400 let mut th = TriggerHandler::new();
401 let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
402 vec![Box::new(PassAgent), Box::new(PassAgent)];
403 let mut mp = vec![ManaPool::default(), ManaPool::default()];
404 let templates = HashMap::new();
405 let templates_variants: HashMap<(String, String), usize> = HashMap::new();
406 let token_fallback: HashMap<String, String> = HashMap::new();
407 let edition_dates: HashMap<String, String> = HashMap::new();
408 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
409 let mut ctx = make_ctx(
410 &mut game,
411 &mut agents,
412 &mut th,
413 &mut mp,
414 &templates,
415 &templates_variants,
416 &token_fallback,
417 &edition_dates,
418 &mut rng_adapter,
419 );
420
421 super::PumpEffect::resolve(&mut ctx, &sa);
422
423 assert!(!ctx.game.card(source).has_indestructible());
424 }
425}