1use super::EffectContext;
8use crate::ability::ability_ir::DefinedRef;
9use crate::parsing::compare::compare_expr;
10use crate::spellability::{build_spell_ability, SpellAbility};
11
12#[manabrew_engine_macros::spell_effect(RepeatEffect)]
16fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
17 let Some(source_id) = sa.source else {
18 return;
19 };
20 let Some(sub_svar_name) = sa.ir.repeat_sub_ability.clone() else {
21 return;
22 };
23 let Some(sub_text) = ctx
24 .game
25 .card(source_id)
26 .get_s_var(&sub_svar_name)
27 .map(str::to_string)
28 else {
29 return;
30 };
31
32 let max_repeat = sa
33 .ir
34 .max_repeat
35 .as_ref()
36 .map(|_| super::resolve_numeric_svar(ctx.game, sa, "MaxRepeat", 0).max(0));
37 if max_repeat == Some(0) {
38 return;
39 }
40
41 let mut count = 0i32;
42 loop {
43 let mut sub_sa = build_spell_ability(ctx.game, source_id, &sub_text, sa.activating_player);
44 if sub_sa.target_chosen.target_card.is_none() {
49 sub_sa.target_chosen.target_card = sa.target_chosen.target_card;
50 }
51 if sub_sa.target_chosen.target_player.is_none() {
52 sub_sa.target_chosen.target_player = sa.target_chosen.target_player;
53 }
54 resolve_sub_chain(ctx, sub_sa);
55 count += 1;
56
57 if ctx.game.game_over {
58 break;
59 }
60 if max_repeat.is_some_and(|max| count >= max) {
61 break;
62 }
63 let should_continue = check_repeat_conditions(ctx, sa);
64 if !should_continue {
65 break;
66 }
67 }
68
69 if let Some(sub_sa) = sa.sub_ability.as_deref() {
70 resolve_sub_chain(ctx, sub_sa.clone());
71 }
72}
73
74fn check_repeat_conditions(ctx: &mut EffectContext, sa: &SpellAbility) -> bool {
75 if sa.ir.repeat_check_svar {
76 let svar_value = super::resolve_numeric_svar(ctx.game, sa, "RepeatCheckSVar", 0);
77 let compare = sa.ir.repeat_svar_compare.as_deref().unwrap_or("GE1");
78 if !compare_expr(svar_value, compare) {
79 return false;
80 }
81 }
82
83 if let (Some(defined), Some(present), Some(compare)) = (
84 sa.ir.repeat_defined.as_deref(),
85 sa.ir.repeat_present.as_deref(),
86 sa.ir.repeat_compare.as_deref(),
87 ) {
88 let Some(source_id) = sa.source else {
89 return false;
90 };
91 let source = ctx.game.card(source_id);
92 let defined_ref = DefinedRef::parse(defined);
93 let cards: Vec<_> = if matches!(defined_ref, DefinedRef::Imprinted) {
94 source.imprinted_cards.clone()
95 } else if matches!(defined_ref, DefinedRef::Remembered) {
96 source.remembered_cards.clone()
97 } else {
98 Vec::new()
99 };
100
101 let present_count = if present.eq_ignore_ascii_case("Card.sharesNameWith Remembered") {
102 let remembered_names: std::collections::HashSet<String> = source
103 .remembered_cards
104 .iter()
105 .map(|&cid| ctx.game.card(cid).card_name.clone())
106 .collect();
107 cards
108 .into_iter()
109 .filter(|&cid| remembered_names.contains(&ctx.game.card(cid).card_name))
110 .count() as i32
111 } else {
112 let selector = crate::parsing::cached_compiled_selector(present);
113 cards
114 .into_iter()
115 .filter(|&cid| {
116 super::matches_valid_cards_for_sa(
117 ctx.game,
118 sa,
119 ctx.game.card(cid),
120 Some(&selector),
121 present,
122 )
123 })
124 .count() as i32
125 };
126
127 if !compare_expr(present_count, compare) {
128 return false;
129 }
130 }
131
132 if sa.ir.repeat_optional {
133 let decider = sa
134 .ir
135 .repeat_optional_decider
136 .as_deref()
137 .and_then(|defined| {
138 crate::ability::ability_utils::resolve_defined_players_with_sa(
139 defined,
140 sa,
141 sa.activating_player,
142 ctx.game,
143 )
144 .into_iter()
145 .next()
146 })
147 .unwrap_or(sa.activating_player);
148 return ctx.agents[decider.index()].confirm_action(
149 decider,
150 Some("Repeat"),
151 "Do you want to repeat this process again?",
152 &[],
153 sa.source,
154 sa.api,
155 );
156 }
157
158 true
159}
160
161fn resolve_sub_chain(ctx: &mut EffectContext, initial: SpellAbility) {
162 let parent_target_card = initial.target_chosen.target_card;
163 let parent_target_player = initial.target_chosen.target_player;
164 let mut cur_opt: Option<SpellAbility> = Some(initial);
165 while let Some(mut cur_sa) = cur_opt {
166 if cur_sa.target_chosen.target_card.is_none() {
167 cur_sa.target_chosen.target_card = parent_target_card;
168 }
169 if cur_sa.target_chosen.target_player.is_none() {
170 cur_sa.target_chosen.target_player = parent_target_player;
171 }
172 super::resolve_effect(ctx, &cur_sa);
173 cur_opt = if super::sub_ability_handled_internally(&cur_sa) {
174 None
175 } else {
176 cur_sa.sub_ability.map(|b| *b)
177 };
178 if ctx.game.game_over {
179 break;
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::ability::spell_ability_effect::SpellAbilityEffect;
188 use crate::agent::{PlayOption, PlayerAgent, TargetChoice};
189 use crate::card::Card;
190 use crate::combat::DefenderId;
191 use crate::game::GameState;
192 use crate::ids::{CardId, PlayerId};
193 use crate::mana::ManaPool;
194 use crate::player::actions::PlayerAction;
195 use crate::trigger::handler::TriggerHandler;
196 use forge_foundation::{CardTypeLine, ColorSet, ManaCost};
197
198 struct RepeatAgent {
199 confirmations: Vec<bool>,
200 }
201
202 impl PlayerAgent for RepeatAgent {
203 fn mulligan_decision(&mut self, _: PlayerId, _: &[CardId], _: u32) -> bool {
204 true
205 }
206 fn choose_action(
207 &mut self,
208 player: PlayerId,
209 action_space: Option<&crate::agent::PriorityActionSpace>,
210 request_action_space: &mut dyn FnMut() -> crate::agent::PriorityActionSpace,
211 ) -> PlayerAction {
212 PlayerAction::PassPriority
213 }
214 fn choose_land_or_spell(&mut self, _: PlayerId) -> Option<bool> {
215 None
216 }
217 fn choose_attackers(
218 &mut self,
219 _: PlayerId,
220 _: &[CardId],
221 _: &[DefenderId],
222 ) -> Vec<(CardId, DefenderId)> {
223 vec![]
224 }
225 fn choose_blockers(
226 &mut self,
227 _: PlayerId,
228 _: &[CardId],
229 _: &[CardId],
230 _: Option<usize>,
231 ) -> Vec<(CardId, CardId)> {
232 vec![]
233 }
234 fn choose_target_player(
235 &mut self,
236 _: PlayerId,
237 valid: &[PlayerId],
238 _: Option<&SpellAbility>,
239 ) -> Option<PlayerId> {
240 valid.first().copied()
241 }
242 fn choose_target_card(
243 &mut self,
244 _: PlayerId,
245 valid: &[CardId],
246 _: Option<&SpellAbility>,
247 ) -> Option<CardId> {
248 valid.first().copied()
249 }
250 fn choose_target_any(
251 &mut self,
252 _: PlayerId,
253 valid_players: &[PlayerId],
254 valid_cards: &[CardId],
255 _: Option<&SpellAbility>,
256 ) -> TargetChoice {
257 if let Some(&pid) = valid_players.first() {
258 TargetChoice::Player(pid)
259 } else if let Some(&cid) = valid_cards.first() {
260 TargetChoice::Card(cid)
261 } else {
262 TargetChoice::None
263 }
264 }
265 fn choose_targets_for(
266 &mut self,
267 _: &mut SpellAbility,
268 _: &GameState,
269 _: &[ManaPool],
270 ) -> bool {
271 true
272 }
273 fn confirm_action(
274 &mut self,
275 _: PlayerId,
276 _: Option<&str>,
277 _: &str,
278 _: &[String],
279 _: Option<crate::ids::CardId>,
280 _: Option<crate::ability::api_type::ApiType>,
281 ) -> bool {
282 if self.confirmations.is_empty() {
283 false
284 } else {
285 self.confirmations.remove(0)
286 }
287 }
288 }
289
290 #[test]
291 fn repeat_optional_executes_until_declined() {
292 let mut game = GameState::new(&["P1", "P2"], 20);
293 let p0 = PlayerId(0);
294 let p1 = PlayerId(1);
295 let source = game.create_card(Card::new(
296 CardId(0),
297 "Repeat Source".into(),
298 p0,
299 CardTypeLine::parse("Instant"),
300 ManaCost::parse("1 B"),
301 ColorSet::BLACK,
302 None,
303 None,
304 vec![],
305 vec![],
306 ));
307 game.card_mut(source).svars.insert(
308 "Loop".to_string(),
309 "DB$ LoseLife | Defined$ Opponent | LifeAmount$ 1".to_string(),
310 );
311
312 let sa = build_spell_ability(
313 &game,
314 source,
315 "SP$ Repeat | RepeatSubAbility$ Loop | RepeatOptional$ True",
316 p0,
317 );
318
319 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
320 Box::new(RepeatAgent {
321 confirmations: vec![true, false],
322 }),
323 Box::new(RepeatAgent {
324 confirmations: vec![],
325 }),
326 ];
327 let mut pools = vec![ManaPool::new(), ManaPool::new()];
328 let mut trigger_handler = TriggerHandler::default();
329 let mut rng = crate::game_rng::ThreadRngAdapter;
330 let start_life = game.player(p1).life;
331
332 RepeatEffect::resolve(
333 &mut EffectContext {
334 game: &mut game,
335 combat: None,
336 agents: &mut agents,
337 trigger_handler: &mut trigger_handler,
338 token_templates: &Default::default(),
339 token_art_variants: &Default::default(),
340 token_fallback: &Default::default(),
341 edition_dates: &Default::default(),
342 mana_pools: &mut pools,
343 parent_target_card: None,
344 rng: &mut rng,
345 },
346 &sa,
347 );
348
349 assert_eq!(game.player(p1).life, start_life - 2);
350 }
351
352 #[test]
353 fn repeat_executes_parent_subability_after_loop() {
354 let mut game = GameState::new(&["P1", "P2"], 20);
355 let p0 = PlayerId(0);
356 let p1 = PlayerId(1);
357 let source = game.create_card(Card::new(
358 CardId(0),
359 "Repeat Source".into(),
360 p0,
361 CardTypeLine::parse("Instant"),
362 ManaCost::parse("1 B"),
363 ColorSet::BLACK,
364 None,
365 None,
366 vec![],
367 vec![],
368 ));
369 game.card_mut(source).svars.insert(
370 "Loop".to_string(),
371 "DB$ LoseLife | Defined$ Opponent | LifeAmount$ 1".to_string(),
372 );
373 game.card_mut(source).svars.insert(
374 "After".to_string(),
375 "DB$ LoseLife | Defined$ Opponent | LifeAmount$ 3".to_string(),
376 );
377
378 let sa = build_spell_ability(
379 &game,
380 source,
381 "SP$ Repeat | RepeatSubAbility$ Loop | MaxRepeat$ 1 | SubAbility$ After",
382 p0,
383 );
384
385 let mut agents: Vec<Box<dyn PlayerAgent>> = vec![
386 Box::new(RepeatAgent {
387 confirmations: vec![],
388 }),
389 Box::new(RepeatAgent {
390 confirmations: vec![],
391 }),
392 ];
393 let mut pools = vec![ManaPool::new(), ManaPool::new()];
394 let mut trigger_handler = TriggerHandler::default();
395 let mut rng = crate::game_rng::ThreadRngAdapter;
396 let start_life = game.player(p1).life;
397
398 RepeatEffect::resolve(
399 &mut EffectContext {
400 game: &mut game,
401 combat: None,
402 agents: &mut agents,
403 trigger_handler: &mut trigger_handler,
404 token_templates: &Default::default(),
405 token_art_variants: &Default::default(),
406 token_fallback: &Default::default(),
407 edition_dates: &Default::default(),
408 mana_pools: &mut pools,
409 parent_target_card: None,
410 rng: &mut rng,
411 },
412 &sa,
413 );
414
415 assert_eq!(game.player(p1).life, start_life - 4);
416 }
417}