1use forge_foundation::ZoneType;
2
3use crate::card::{valid_filter, Card};
4use crate::game::GameState;
5use crate::ids::PlayerId;
6use crate::staticability::StaticAbility;
7use crate::staticability::StaticMode;
8
9fn next_player_in_direction(game: &GameState, from: PlayerId, direction: &str) -> PlayerId {
10 let alive: Vec<PlayerId> = game
11 .player_order
12 .iter()
13 .copied()
14 .filter(|&pid| game.player(pid).is_alive())
15 .collect();
16 if alive.is_empty() {
17 return from;
18 }
19 let Some(idx) = alive.iter().position(|&pid| pid == from) else {
20 return alive[0];
21 };
22 let len = alive.len();
23 let is_left = direction.eq_ignore_ascii_case("Left");
24 let next_idx = if is_left {
25 (idx + 1) % len
26 } else {
27 (idx + len - 1) % len
28 };
29 alive[next_idx]
30}
31
32fn nearest_opponent_in_direction(
33 game: &GameState,
34 controller: PlayerId,
35 direction: &str,
36) -> Option<PlayerId> {
37 let alive_count = game
38 .player_order
39 .iter()
40 .filter(|&&pid| game.player(pid).is_alive())
41 .count();
42 if alive_count <= 1 {
43 return None;
44 }
45 let mut next = controller;
46 for _ in 0..alive_count {
47 next = next_player_in_direction(game, next, direction);
48 if next != controller {
49 return Some(next);
50 }
51 }
52 None
53}
54
55pub fn cant_attack(game: &GameState, cards: &[Card], attacker: &Card, defender: PlayerId) -> bool {
60 if attacker.has_keyword("CARDNAME can't attack.")
62 || attacker.has_keyword("CARDNAME can't attack or block.")
63 {
64 return true;
65 }
66
67 if attacker.detained {
69 return true;
70 }
71
72 for source in cards.iter().filter(|c| c.zone.is_static_ability_source()) {
73 for st_ab in source
74 .static_abilities
75 .iter()
76 .filter(|sa| sa.check_conditions_full(&StaticMode::CantAttack, source, game))
77 {
78 if apply_cant_attack_ability(game, st_ab, attacker, source, defender, cards) {
79 return true;
80 }
81 }
82 }
83 false
84}
85
86pub fn apply_cant_attack_ability(
88 game: &GameState,
89 st_ab: &StaticAbility,
90 card: &Card,
91 source: &Card,
92 defender: PlayerId,
93 cards: &[Card],
94) -> bool {
95 if !valid_filter::matches_valid_card_selector_opt_in_game(
96 st_ab.ir.valid_card.as_ref(),
97 card,
98 source,
99 game,
100 ) {
101 return false;
102 }
103
104 if st_ab.ignore_effect_cards.contains(&card.id) {
106 return false;
107 }
108
109 if !valid_filter::matches_valid_player_opt(
113 st_ab.ir.target_text.as_deref(),
114 defender,
115 source.controller,
116 ) {
117 return false;
118 }
119
120 if st_ab
123 .ir
124 .kw_text
125 .as_deref()
126 .is_some_and(|v| v.eq_ignore_ascii_case("Defender"))
127 && can_attack_defender(game, cards, card, defender)
128 {
129 return false;
130 }
131
132 if st_ab.ir.defender_not_nearest_to_you_in_chosen_direction {
133 let Some(direction) = source.svars.get("ChosenDirection") else {
135 return false;
136 };
137 if nearest_opponent_in_direction(game, card.controller, direction) == Some(defender) {
138 return false;
139 }
140 }
141
142 if let Some(unless_type) = st_ab.ir.unless_defender_text.as_deref() {
144 if valid_filter::matches_valid_player(unless_type, defender, source.controller) {
145 return false;
146 }
147 }
148
149 true
150}
151
152pub fn can_attack_defender(
157 game: &GameState,
158 cards: &[Card],
159 card: &Card,
160 defender: PlayerId,
161) -> bool {
162 for source in cards.iter().filter(|c| c.zone.is_static_ability_source()) {
163 for st_ab in source
164 .static_abilities
165 .iter()
166 .filter(|sa| sa.check_conditions_full(&StaticMode::CanAttackDefender, source, game))
167 {
168 if apply_can_attack_defender_ability(game, st_ab, card, source, defender) {
169 return true;
170 }
171 }
172 }
173 false
174}
175
176pub fn apply_can_attack_defender_ability(
178 game: &GameState,
179 st_ab: &StaticAbility,
180 card: &Card,
181 source: &Card,
182 defender: PlayerId,
183) -> bool {
184 if !valid_filter::matches_valid_card_selector_opt_in_game(
185 st_ab.ir.valid_card.as_ref(),
186 card,
187 source,
188 game,
189 ) {
190 return false;
191 }
192
193 if !valid_filter::matches_valid_player_selector_opt(
195 st_ab.ir.valid_attacked.as_ref(),
196 defender,
197 source.controller,
198 ) {
199 return false;
200 }
201
202 true
203}
204
205pub fn cant_block(game: &GameState, cards: &[Card], blocker: &Card) -> bool {
210 if blocker.detained {
212 return true;
213 }
214
215 for source in cards
217 .iter()
218 .filter(|c| c.zone.is_static_ability_source() || c.id == blocker.id)
219 {
220 for st_ab in source
221 .static_abilities
222 .iter()
223 .filter(|sa| sa.check_conditions_full(&StaticMode::CantBlock, source, game))
224 {
225 if apply_cant_block_ability(game, st_ab, blocker, source) {
226 return true;
227 }
228 }
229 }
230 false
231}
232
233pub fn apply_cant_block_ability(
235 game: &GameState,
236 st_ab: &StaticAbility,
237 blocker: &Card,
238 source: &Card,
239) -> bool {
240 if !valid_filter::matches_valid_card_selector_opt_in_game(
241 st_ab.ir.valid_card.as_ref(),
242 blocker,
243 source,
244 game,
245 ) {
246 return false;
247 }
248
249 if st_ab.ignore_effect_cards.contains(&blocker.id) {
251 return false;
252 }
253
254 true
255}
256
257pub fn cant_block_by(
262 game: &GameState,
263 cards: &[Card],
264 attacker: &Card,
265 blocker: Option<&Card>,
266) -> bool {
267 for source in cards.iter().filter(|c| {
269 c.zone.is_static_ability_source()
270 || c.id == attacker.id
271 || blocker.is_some_and(|b| c.id == b.id)
272 }) {
273 for st_ab in source
274 .static_abilities
275 .iter()
276 .filter(|sa| sa.check_conditions_full(&StaticMode::CantBlockBy, source, game))
277 {
278 if apply_cant_block_by_ability(game, st_ab, attacker, blocker, source, cards) {
279 return true;
280 }
281 }
282 }
283 false
284}
285
286pub fn apply_cant_block_by_ability(
289 game: &GameState,
290 st_ab: &StaticAbility,
291 attacker: &Card,
292 blocker: Option<&Card>,
293 source: &Card,
294 cards: &[Card],
295) -> bool {
296 if !valid_filter::matches_valid_card_selector_opt_in_game(
297 st_ab.ir.valid_attacker.as_ref(),
298 attacker,
299 source,
300 game,
301 ) {
302 return false;
303 }
304
305 if let Some(valid_blocker_param) = st_ab.ir.valid_blocker.as_ref() {
307 let mut still_block = true;
308 for alternative in &valid_blocker_param.alternatives {
309 if let Some(b) = blocker {
310 let matches_blocker =
311 crate::parsing::CompiledSelector::from_alternatives(vec![alternative.clone()]);
312 if valid_filter::matches_valid_card_selector_in_game(
313 &matches_blocker,
314 b,
315 source,
316 game,
317 ) {
318 still_block = false;
319 if alternative
322 .parts
323 .iter()
324 .any(|part| part.value.eq_ignore_ascii_case("withoutReach"))
325 && can_block_if_reach(game, cards, attacker, b)
326 {
327 still_block = true;
328 }
329 if !still_block {
330 break;
331 }
332 }
333 }
334 }
335 if still_block {
336 return false;
337 }
338 }
339
340 if let Some(blocker_card) = blocker {
342 if !valid_filter::matches_valid_card_selector_opt_in_game(
343 st_ab.ir.valid_attacker_relative.as_ref(),
344 attacker,
345 blocker_card,
346 game,
347 ) {
348 return false;
349 }
350 } else if st_ab.ir.has_valid_attacker_relative {
351 return false;
352 }
353
354 if let Some(blocker_card) = blocker {
356 if !valid_filter::matches_valid_card_selector_opt_in_game(
357 st_ab.ir.valid_blocker_relative.as_ref(),
358 blocker_card,
359 attacker,
360 game,
361 ) {
362 return false;
363 }
364 } else if st_ab.ir.has_valid_blocker_relative {
365 return false;
366 }
367
368 if let Some(blocker_card) = blocker {
370 if !valid_filter::matches_valid_player_selector_opt(
371 st_ab.ir.valid_defender.as_ref(),
372 blocker_card.controller,
373 source.controller,
374 ) {
375 return false;
376 }
377 } else {
378 return false;
380 }
381
382 if let Some(kw_val) = st_ab.ir.kw_text.as_deref() {
384 if kw_val.contains("Landwalk") || kw_val.contains("landwalk") {
385 if let Some(blocker_card) = blocker {
386 if crate::staticability::static_ability_ignore_landwalk::ignore_land_walk(
387 cards,
388 attacker,
389 blocker_card,
390 kw_val,
391 ) {
392 return false;
393 }
394 }
395 }
396 }
397
398 true
399}
400
401pub fn can_block_if_reach(
406 game: &GameState,
407 cards: &[Card],
408 attacker: &Card,
409 blocker: &Card,
410) -> bool {
411 for source in cards.iter().filter(|c| c.zone.is_static_ability_source()) {
412 for st_ab in source
413 .static_abilities
414 .iter()
415 .filter(|sa| sa.check_conditions_full(&StaticMode::CanBlockIfReach, source, game))
416 {
417 if apply_can_block_if_reach_ability(game, st_ab, attacker, blocker, source) {
418 return true;
419 }
420 }
421 }
422 false
423}
424
425pub fn apply_can_block_if_reach_ability(
427 game: &GameState,
428 st_ab: &StaticAbility,
429 attacker: &Card,
430 blocker: &Card,
431 source: &Card,
432) -> bool {
433 if !valid_filter::matches_valid_card_selector_opt_in_game(
434 st_ab.ir.valid_attacker.as_ref(),
435 attacker,
436 source,
437 game,
438 ) {
439 return false;
440 }
441 if !valid_filter::matches_valid_card_selector_opt_in_game(
442 st_ab.ir.valid_blocker.as_ref(),
443 blocker,
444 source,
445 game,
446 ) {
447 return false;
448 }
449 true
450}
451
452pub fn can_block_tapped(game: &GameState, cards: &[Card], card: &Card) -> bool {
457 for source in cards.iter().filter(|c| c.zone.is_static_ability_source()) {
458 for st_ab in source
459 .static_abilities
460 .iter()
461 .filter(|sa| sa.check_conditions_full(&StaticMode::BlockTapped, source, game))
462 {
463 if apply_block_tapped(game, st_ab, card, source) {
464 return true;
465 }
466 }
467 }
468 false
469}
470
471fn apply_block_tapped(game: &GameState, st_ab: &StaticAbility, card: &Card, source: &Card) -> bool {
473 if !valid_filter::matches_valid_card_selector_opt_in_game(
474 st_ab.ir.valid_card.as_ref(),
475 card,
476 source,
477 game,
478 ) {
479 return false;
480 }
481 true
482}
483
484pub fn can_attack_haste(
489 game: &GameState,
490 cards: &[Card],
491 attacker: &Card,
492 _defender: PlayerId,
493) -> bool {
494 if !attacker.summoning_sick {
496 return true;
497 }
498
499 for source in cards.iter().filter(|c| c.zone.is_static_ability_source()) {
500 for st_ab in source
501 .static_abilities
502 .iter()
503 .filter(|sa| sa.check_conditions_full(&StaticMode::CanAttackIfHaste, source, game))
504 {
505 if apply_can_attack_haste_ability(game, st_ab, attacker, _defender, source) {
506 return true;
507 }
508 }
509 }
510 false
511}
512
513pub fn apply_can_attack_haste_ability(
515 game: &GameState,
516 st_ab: &StaticAbility,
517 card: &Card,
518 defender: PlayerId,
519 source: &Card,
520) -> bool {
521 if !valid_filter::matches_valid_card_selector_opt_in_game(
522 st_ab.ir.valid_card.as_ref(),
523 card,
524 source,
525 game,
526 ) {
527 return false;
528 }
529
530 if !valid_filter::matches_valid_player_selector_opt(
532 st_ab.ir.valid_target.as_ref(),
533 defender,
534 source.controller,
535 ) {
536 return false;
537 }
538
539 true
540}
541
542pub fn get_min_max_blocker(
547 game: &GameState,
548 cards: &[Card],
549 attacker: &Card,
550 _defender: PlayerId,
551) -> (i32, i32) {
552 let mut min: i32 = 1;
553 let mut max: i32 = i32::MAX;
554
555 if attacker.has_menace() {
557 min = 2;
558 }
559
560 for source in cards.iter().filter(|c| c.zone.is_static_ability_source()) {
561 for st_ab in source
562 .static_abilities
563 .iter()
564 .filter(|sa| sa.check_conditions_full(&StaticMode::MinMaxBlocker, source, game))
565 {
566 apply_min_max_blocker_ability(
567 game, st_ab, attacker, source, _defender, cards, &mut min, &mut max,
568 );
569 }
570 }
571
572 (min, max)
573}
574
575pub fn apply_min_max_blocker_ability(
577 game: &GameState,
578 st_ab: &StaticAbility,
579 attacker: &Card,
580 source: &Card,
581 defender: PlayerId,
582 cards: &[Card],
583 min: &mut i32,
584 max: &mut i32,
585) {
586 if !valid_filter::matches_valid_card_selector_opt_in_game(
587 st_ab.ir.valid_card.as_ref(),
588 attacker,
589 source,
590 game,
591 ) {
592 return;
593 }
594
595 if let Some(min_val) = st_ab.ir.min_text.as_deref() {
596 if min_val == "All" {
597 let creature_count = cards
600 .iter()
601 .filter(|c| {
602 c.controller == defender && c.zone == ZoneType::Battlefield && c.is_creature()
603 })
604 .count() as i32;
605 *min = creature_count;
606 } else if let Some(val) = resolve_amount_expr(None, source, min_val) {
607 *min = val;
608 }
609 }
610
611 if let Some(max_val) = st_ab.ir.max_text.as_deref() {
612 if let Some(val) = resolve_amount_expr(None, source, max_val) {
613 *max = val;
614 }
615 }
616}
617
618pub fn attack_vigilance(game: &GameState, cards: &[Card], card: &Card) -> bool {
623 for source in cards.iter().filter(|c| c.zone.is_static_ability_source()) {
624 for st_ab in source
625 .static_abilities
626 .iter()
627 .filter(|sa| sa.check_conditions_full(&StaticMode::AttackVigilance, source, game))
628 {
629 if apply_attack_vigilance_ability(game, st_ab, card, source) {
630 return true;
631 }
632 }
633 }
634 false
635}
636
637pub fn apply_attack_vigilance_ability(
639 game: &GameState,
640 st_ab: &StaticAbility,
641 card: &Card,
642 source: &Card,
643) -> bool {
644 if !valid_filter::matches_valid_card_selector_opt_in_game(
645 st_ab.ir.valid_card.as_ref(),
646 card,
647 source,
648 game,
649 ) {
650 return false;
651 }
652 true
653}
654
655pub fn get_attack_cost(
661 st_ab: &StaticAbility,
662 attacker: &Card,
663 target: PlayerId,
664 source: &Card,
665) -> Option<String> {
666 if !valid_filter::matches_valid_card_selector_opt(
667 st_ab.ir.valid_card.as_ref(),
668 attacker,
669 source,
670 ) {
671 return None;
672 }
673
674 if !valid_filter::matches_valid_player_opt(
675 st_ab.ir.target_text.as_deref(),
676 target,
677 source.controller,
678 ) {
679 return None;
680 }
681
682 let mut cost_string = st_ab.ir.cost.clone()?;
683 if let Some(svar_expr) = source.svars.get(&cost_string) {
684 let add_x = cost_string.starts_with('X');
685 let amount = crate::svar::evaluate_svar(
686 svar_expr,
687 &crate::spellability::SpellAbility::new_empty(Some(source.id), source.controller),
688 );
689 cost_string = amount.to_string();
690 if add_x {
691 cost_string.push_str(" X");
692 }
693 }
694
695 if st_ab.ir.trigger {
696 }
699
700 Some(cost_string)
701}
702
703pub fn get_block_cost(
709 st_ab: &StaticAbility,
710 blocker: &Card,
711 attacker_player: PlayerId,
712 source: &Card,
713) -> Option<String> {
714 if !valid_filter::matches_valid_card_selector_opt(st_ab.ir.valid_card.as_ref(), blocker, source)
715 {
716 return None;
717 }
718
719 if !valid_filter::matches_valid_player_opt(
722 st_ab.ir.attacker_text.as_deref(),
723 attacker_player,
724 source.controller,
725 ) {
726 return None;
727 }
728
729 let mut cost_string = st_ab.ir.cost.clone()?;
730 if let Some(svar_expr) = source.svars.get(&cost_string) {
731 let add_x = cost_string.starts_with('X');
732 let amount = crate::svar::evaluate_svar(
733 svar_expr,
734 &crate::spellability::SpellAbility::new_empty(Some(source.id), source.controller),
735 );
736 cost_string = amount.to_string();
737 if add_x {
738 cost_string.push_str(" X");
739 }
740 }
741
742 Some(cost_string)
743}
744
745fn resolve_amount_expr(game: Option<&GameState>, source: &Card, expr: &str) -> Option<i32> {
746 if let Ok(v) = expr.parse::<i32>() {
747 return Some(v);
748 }
749 let svar_expr = source.svars.get(expr)?;
750 if let Some(g) = game {
751 if svar_expr.starts_with("Count$") {
752 return Some(crate::svar::resolve_count_svar(
753 svar_expr,
754 g,
755 source.id,
756 source.controller,
757 ));
758 }
759 }
760 Some(crate::svar::evaluate_svar(
761 svar_expr,
762 &crate::spellability::SpellAbility::new_empty(Some(source.id), source.controller),
763 ))
764}