manabrew_engine/ability/effects/
counters_put_effect.rs1use forge_foundation::ZoneType;
2
3use super::{parse_counter_type, resolve_defined_player, resolve_numeric_svar, EffectContext};
4use crate::ability::ability_ir::DefinedRef;
5use crate::card::CounterType;
6use crate::event::RunParams;
7use crate::parsing::keys;
8use crate::replacement::replacement_handler::{apply_replacements_with_agents, ReplacementEvent};
9use crate::spellability::SpellAbility;
10use crate::trigger::TriggerType;
11
12#[manabrew_engine_macros::spell_effect(CountersPutEffect)]
16fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
17 let counter_type_str = sa.ir.counter_type_text.as_deref().unwrap_or("P1P1");
18 let counter_type = if matches_choose_from_list_path(sa) {
25 let placer_controller = sa
26 .source
27 .map(|id| ctx.game.card(id).controller)
28 .unwrap_or_else(|| ctx.game.player_order[0]);
29 let options: Vec<crate::card::CounterType> = counter_type_str
30 .split(',')
31 .map(str::trim)
32 .filter(|s| !s.is_empty())
33 .map(parse_counter_type)
34 .collect();
35 if options.is_empty() {
36 return;
37 }
38 ctx.agents[placer_controller.index()].snapshot_state(ctx.game, ctx.mana_pools);
39 match ctx.agents[placer_controller.index()].choose_counter_type(
40 placer_controller,
41 &options,
42 "Select counter type",
43 ) {
44 Some(chosen) => chosen,
45 None => return,
46 }
47 } else {
48 sa.ir
49 .counter_type
50 .clone()
51 .unwrap_or_else(|| parse_counter_type(counter_type_str))
52 };
53 let mut count = resolve_numeric_svar(ctx.game, sa, keys::COUNTER_NUM, 1);
55 if sa.ir.modular && sa.trigger_remembered_amount > 0 {
59 count = sa.trigger_remembered_amount;
60 }
61
62 let source_controller = sa
64 .source
65 .map(|id| ctx.game.card(id).controller)
66 .unwrap_or_else(|| ctx.game.player_order[0]);
67 if let Some(defined) = sa.defined() {
70 if let Some(target_player) = resolve_defined_player(defined, source_controller, ctx.game) {
71 match &counter_type {
72 CounterType::Named(name) if name == "ENERGY" => {
73 ctx.game.player_add_energy(target_player, count);
74 return;
75 }
76 _ => {
77 }
81 }
82 }
83 }
84
85 let uses_targeting = sa.target_restrictions.is_some();
89 let target_id = if uses_targeting && sa.ir.defined.is_none() {
90 sa.target_chosen.target_card
92 } else {
93 match sa.defined_ref() {
94 Some(
98 DefinedRef::TriggeredTarget
99 | DefinedRef::TriggeredTargetLkiCopy
100 | DefinedRef::Targeted,
101 ) => sa.target_chosen.target_card,
102 _ => sa.source,
103 }
104 };
105 let Some(card_id) = target_id else { return };
106 if matches!(sa.defined_ref(), None | Some(DefinedRef::SelfCard)) && sa.source == Some(card_id) {
107 if let Some(created_at) = sa.source_zone_timestamp {
110 if ctx.game.card(card_id).zone_timestamp != created_at {
111 return;
112 }
113 }
114 }
115 if ctx.game.card(card_id).zone != ZoneType::Battlefield {
116 return;
117 }
118
119 let is_adapt = sa.ir.adapt;
122 if is_adapt {
123 let current = ctx
124 .game
125 .card(card_id)
126 .counter_count(&crate::card::CounterType::P1P1);
127 if current > 0 {
128 return;
129 }
130 }
131
132 let is_monstrosity = sa.ir.monstrosity;
133 if is_monstrosity && ctx.game.card(card_id).monstrous {
134 return;
135 }
136
137 let is_bloodthirst = sa.ir.bloodthirst;
138 if is_bloodthirst && !ctx.game.player_has_bloodthirst(source_controller) {
139 return;
140 }
141
142 if crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
143 &ctx.game.cards,
144 ctx.game.card(card_id),
145 &counter_type,
146 ) {
147 return;
148 }
149 if let Some(max) = crate::staticability::static_ability_max_counter::max_counter(
150 &ctx.game.cards,
151 ctx.game.card(card_id),
152 &counter_type,
153 ) {
154 let current = ctx.game.card(card_id).counter_count(&counter_type);
155 if current >= max {
156 return;
157 }
158 }
159 let mut event = ReplacementEvent::AddCounter {
161 target: card_id,
162 counter_type: counter_type.clone(),
163 count,
164 is_effect: true,
165 };
166 apply_replacements_with_agents(&mut *ctx.game, ctx.agents, &mut event);
167 let count = if let ReplacementEvent::AddCounter {
168 count: final_count, ..
169 } = event
170 {
171 final_count
172 } else {
173 count
174 };
175 let cause_player = ctx.game.card(card_id).controller;
176 ctx.game.card_mut(card_id).add_counter(&counter_type, count);
177
178 if sa.ir.renown {
180 ctx.game.card_mut(card_id).set_renowned(true);
181 }
182
183 ctx.trigger_handler.run_trigger(
185 TriggerType::CounterAdded,
186 RunParams {
187 card: Some(card_id),
188 counter_type: Some(format!("{:?}", counter_type)),
189 counter_amount: Some(count),
190 cause_player: Some(cause_player),
191 ..Default::default()
192 },
193 false,
194 );
195 ctx.trigger_handler.run_trigger(
199 TriggerType::CounterAddedOnce,
200 RunParams {
201 card: Some(card_id),
202 counter_type: Some(format!("{:?}", counter_type)),
203 counter_amount: Some(count),
204 cause_player: Some(cause_player),
205 ..Default::default()
206 },
207 false,
208 );
209
210 if is_monstrosity {
211 ctx.game.card_mut(card_id).set_monstrous(true);
212 ctx.trigger_handler.run_trigger(
213 TriggerType::BecomeMonstrous,
214 RunParams {
215 card: Some(card_id),
216 counter_amount: Some(count),
217 ..Default::default()
218 },
219 false,
220 );
221 }
222}
223
224fn matches_choose_from_list_path(sa: &SpellAbility) -> bool {
230 #[allow(dead_code)]
231 const SKIP_PARAMS: &[&str] = &[
232 "EachExistingCounter",
233 "EachFromSource",
234 "UniqueType",
235 "CounterTypePerDefined",
236 "CounterTypes",
237 "ChooseDifferent",
238 "PutOnEachOther",
239 "PutOnDefined",
240 "TriggeredCounterMap",
241 "SharedKeywords",
242 ];
243 sa.ir.simple_counter_type_choice_path
244}
245
246#[cfg(test)]
247mod tests {
248 use crate::ability::spell_ability_effect::SpellAbilityEffect;
249 use std::collections::HashMap;
250
251 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
252
253 use crate::ability::effects::EffectContext;
254 use crate::agent::PassAgent;
255 use crate::card::{Card, CounterType};
256 use crate::game::GameState;
257 use crate::ids::{CardId, PlayerId};
258 use crate::mana::ManaPool;
259 use crate::spellability::SpellAbility;
260 use crate::trigger::handler::TriggerHandler;
261
262 fn make_creature(game: &mut GameState, owner: PlayerId, name: &str) -> CardId {
263 let card = Card::new(
264 CardId(0),
265 name.to_string(),
266 owner,
267 CardTypeLine::parse("Creature - Golem"),
268 ManaCost::parse("5"),
269 ColorSet::COLORLESS,
270 Some(3),
271 Some(3),
272 vec![],
273 vec![],
274 );
275 game.create_card(card)
276 }
277
278 fn make_ctx<'a>(
279 game: &'a mut GameState,
280 agents: &'a mut Vec<Box<dyn crate::agent::PlayerAgent>>,
281 trigger_handler: &'a mut TriggerHandler,
282 mana_pools: &'a mut Vec<ManaPool>,
283 token_templates: &'a HashMap<String, Card>,
284 token_art_variants: &'a HashMap<(String, String), usize>,
285 token_fallback: &'a HashMap<String, String>,
286 edition_dates: &'a HashMap<String, String>,
287 rng: &'a mut dyn crate::game_rng::GameRng,
288 ) -> EffectContext<'a> {
289 EffectContext {
290 game,
291 combat: None,
292 agents,
293 trigger_handler,
294 token_templates,
295 token_art_variants,
296 token_fallback,
297 edition_dates,
298 mana_pools,
299 parent_target_card: None,
300 rng,
301 }
302 }
303
304 #[test]
305 fn monstrosity_only_applies_once() {
306 let mut game = GameState::new(&["Alice", "Bob"], 20);
307 let p0 = PlayerId(0);
308 let clay_golem = make_creature(&mut game, p0, "Clay Golem");
309 game.move_card(clay_golem, ZoneType::Battlefield, p0);
310
311 let sa = SpellAbility::new_simple(
312 Some(clay_golem),
313 p0,
314 "AB$ PutCounter | Defined$ Self | Monstrosity$ True | CounterNum$ 4 | CounterType$ P1P1",
315 );
316
317 let mut trigger_handler = TriggerHandler::new();
318 let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
319 vec![Box::new(PassAgent), Box::new(PassAgent)];
320 let mut mana_pools = vec![ManaPool::default(), ManaPool::default()];
321 let token_templates = HashMap::new();
322 let templates_variants: HashMap<(String, String), usize> = HashMap::new();
323 let token_fallback: HashMap<String, String> = HashMap::new();
324 let edition_dates: HashMap<String, String> = HashMap::new();
325 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
326 let mut ctx = make_ctx(
327 &mut game,
328 &mut agents,
329 &mut trigger_handler,
330 &mut mana_pools,
331 &token_templates,
332 &templates_variants,
333 &token_fallback,
334 &edition_dates,
335 &mut rng_adapter,
336 );
337
338 super::CountersPutEffect::resolve(&mut ctx, &sa);
339 assert_eq!(
340 ctx.game.card(clay_golem).counter_count(&CounterType::P1P1),
341 4
342 );
343 assert!(ctx.game.card(clay_golem).monstrous);
344
345 super::CountersPutEffect::resolve(&mut ctx, &sa);
346 assert_eq!(
347 ctx.game.card(clay_golem).counter_count(&CounterType::P1P1),
348 4
349 );
350 assert!(ctx.game.card(clay_golem).monstrous);
351 }
352
353 #[test]
354 fn monstrous_resets_after_leaving_battlefield() {
355 let mut game = GameState::new(&["Alice", "Bob"], 20);
356 let p0 = PlayerId(0);
357 let clay_golem = make_creature(&mut game, p0, "Clay Golem");
358 game.move_card(clay_golem, ZoneType::Battlefield, p0);
359 game.card_mut(clay_golem).set_monstrous(true);
360
361 game.move_card(clay_golem, ZoneType::Hand, p0);
362
363 assert!(!game.card(clay_golem).monstrous);
364 }
365}