1pub mod attack_constraints;
2pub mod attack_cost;
3pub mod attack_requirement;
4pub mod attack_restriction;
5pub mod attack_restriction_type;
6pub mod attacking_band;
7pub mod block_cost;
8pub mod combat_lki;
9pub mod combat_util;
10pub mod global_attack_restrictions;
11pub mod selector_domain;
12
13use std::collections::{HashMap, HashSet};
14
15use forge_foundation::ZoneType;
16use serde::{Deserialize, Serialize};
17
18use crate::agent::PlayerAgent;
19use crate::game::GameState;
20use crate::ids::{CardId, PlayerId};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum DefenderId {
25 Player(PlayerId),
26 Permanent(CardId),
27}
28
29impl DefenderId {
30 pub fn controlling_player(&self, game: &GameState) -> PlayerId {
33 match self {
34 DefenderId::Player(pid) => *pid,
35 DefenderId::Permanent(cid) => game.card(*cid).controller,
36 }
37 }
38
39 pub fn as_player(&self) -> Option<PlayerId> {
41 match self {
42 DefenderId::Player(pid) => Some(*pid),
43 DefenderId::Permanent(_) => None,
44 }
45 }
46}
47
48pub use combat_lki::CombatLki;
49
50#[derive(Debug, Clone)]
53pub struct CombatDamageEvent {
54 pub source: CardId,
55 pub target_player: Option<PlayerId>,
56 pub target_card: Option<CardId>,
57 pub amount: i32,
58 pub is_combat: bool,
59 pub lifelink_player: Option<PlayerId>,
60 pub lifelink_amount: i32,
61}
62
63pub struct CombatDamageResolution {
64 pub events: Vec<CombatDamageEvent>,
65 pub counter_table: crate::game_entity_counter_table::GameEntityCounterTable,
66}
67
68#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70pub struct CombatState {
71 pub attacking_player: Option<PlayerId>,
73 pub defending_player: Option<PlayerId>,
75 pub attackers: Vec<(CardId, DefenderId)>,
77 #[serde(default)]
79 pub attacker_zone_timestamps: HashMap<CardId, u64>,
80 pub blockers: Vec<(CardId, CardId)>,
82 #[serde(default)]
85 pub blocked_attackers: HashSet<CardId>,
86 #[serde(default)]
88 pub blocker_zone_timestamps: HashMap<CardId, u64>,
89 #[serde(default)]
93 pub damage_order: HashMap<CardId, Vec<CardId>>,
94 #[serde(skip)]
97 pub lki_cache: HashMap<CardId, CombatLki>,
98}
99
100impl CombatState {
101 pub fn new() -> Self {
102 Self::default()
103 }
104
105 pub fn clear(&mut self) {
106 self.attacking_player = None;
107 self.defending_player = None;
108 self.attackers.clear();
109 self.attacker_zone_timestamps.clear();
110 self.blockers.clear();
111 self.blocked_attackers.clear();
112 self.blocker_zone_timestamps.clear();
113 self.damage_order.clear();
114 self.lki_cache.clear();
115 }
116
117 pub fn clear_with_cards(&mut self, cards: &mut [crate::card::Card]) {
119 for &(attacker_id, _) in &self.attackers {
120 cards[attacker_id.index()].attacking_player = None;
121 }
122 let lki = std::mem::take(&mut self.lki_cache);
124 self.clear();
125 self.lki_cache = lki;
126 }
127
128 pub fn declare_attacker(
129 &mut self,
130 attacker: CardId,
131 defending: DefenderId,
132 zone_timestamp: u64,
133 ) {
134 self.attackers.push((attacker, defending));
135 self.attacker_zone_timestamps
136 .insert(attacker, zone_timestamp);
137 }
138
139 pub fn declare_blocker(&mut self, blocker: CardId, attacker: CardId, zone_timestamp: u64) {
140 self.blockers.push((blocker, attacker));
141 self.blocked_attackers.insert(attacker);
142 self.blocker_zone_timestamps.insert(blocker, zone_timestamp);
143 }
144
145 pub fn is_attacking(&self, card: CardId) -> bool {
146 self.attackers.iter().any(|(a, _)| *a == card)
147 }
148
149 pub fn is_blocked(&self, attacker: CardId) -> bool {
150 self.blockers.iter().any(|(_, a)| *a == attacker)
151 }
152
153 pub fn was_blocked_this_combat(&self, attacker: CardId) -> bool {
155 self.blocked_attackers.contains(&attacker) || self.is_blocked(attacker)
156 }
157
158 pub fn get_blockers_for(&self, attacker: CardId) -> Vec<CardId> {
159 self.blockers
160 .iter()
161 .filter(|(_, a)| *a == attacker)
162 .map(|(b, _)| *b)
163 .collect()
164 }
165
166 pub fn get_attackers_for(&self, blocker: CardId) -> Vec<CardId> {
167 self.blockers
168 .iter()
169 .filter(|(b, _)| *b == blocker)
170 .map(|(_, a)| *a)
171 .collect()
172 }
173
174 pub fn has_attackers(&self) -> bool {
175 !self.attackers.is_empty()
176 }
177
178 pub fn save_lki(&mut self, card_id: CardId) -> Option<CombatLki> {
180 if let Some((_, defender)) = self.attackers.iter().find(|(a, _)| *a == card_id) {
182 let lki = CombatLki {
183 is_attacker: true,
184 defender: Some(*defender),
185 blocked_attackers: vec![],
186 };
187 self.lki_cache.insert(card_id, lki.clone());
188 return Some(lki);
189 }
190 let blocked: Vec<CardId> = self
192 .blockers
193 .iter()
194 .filter(|(b, _)| *b == card_id)
195 .map(|(_, a)| *a)
196 .collect();
197 if !blocked.is_empty() {
198 let lki = CombatLki {
199 is_attacker: false,
200 defender: None,
201 blocked_attackers: blocked,
202 };
203 self.lki_cache.insert(card_id, lki.clone());
204 return Some(lki);
205 }
206 None
207 }
208
209 pub fn get_combat_lki(&self, card_id: CardId) -> Option<&CombatLki> {
211 self.lki_cache.get(&card_id)
212 }
213
214 pub fn was_attacking(&self, card_id: CardId) -> bool {
216 self.attackers.iter().any(|(a, _)| *a == card_id)
217 || self.lki_cache.get(&card_id).is_some_and(|l| l.is_attacker)
218 }
219
220 pub fn was_blocking(&self, card_id: CardId) -> bool {
222 self.blockers.iter().any(|(b, _)| *b == card_id)
223 || self.lki_cache.get(&card_id).is_some_and(|l| !l.is_attacker)
224 }
225
226 pub fn remove_absent_combatants(&mut self, cards: &[crate::card::Card]) -> bool {
232 let before_attackers = self.attackers.len();
233 let before_blockers = self.blockers.len();
234
235 self.attackers.retain(|&(id, _)| {
236 let card = &cards[id.index()];
237 let timestamp_ok = self
238 .attacker_zone_timestamps
239 .get(&id)
240 .map(|&ts| ts == card.zone_timestamp)
241 .unwrap_or(true);
242 card.zone == ZoneType::Battlefield && card.is_creature() && timestamp_ok
243 });
244 self.blockers.retain(|&(id, _)| {
245 let card = &cards[id.index()];
246 let timestamp_ok = self
247 .blocker_zone_timestamps
248 .get(&id)
249 .map(|&ts| ts == card.zone_timestamp)
250 .unwrap_or(true);
251 card.zone == ZoneType::Battlefield && card.is_creature() && timestamp_ok
252 });
253
254 let attacker_ids: HashSet<CardId> = self.attackers.iter().map(|(a, _)| *a).collect();
255 self.attacker_zone_timestamps
256 .retain(|attacker_id, _| attacker_ids.contains(attacker_id));
257
258 self.damage_order.retain(|k, _| attacker_ids.contains(k));
260
261 let blocker_ids: HashSet<CardId> = self.blockers.iter().map(|(b, _)| *b).collect();
263 self.blocker_zone_timestamps
264 .retain(|blocker_id, _| blocker_ids.contains(blocker_id));
265 for order in self.damage_order.values_mut() {
266 order.retain(|b| blocker_ids.contains(b));
267 }
268
269 self.attackers.len() != before_attackers || self.blockers.len() != before_blockers
270 }
271
272 pub fn has_first_strikers(&self, game: &GameState) -> bool {
274 for &(attacker_id, _) in &self.attackers {
275 if !game.card_is_in_zone(attacker_id, ZoneType::Battlefield) {
276 continue;
277 }
278 let card = game.card(attacker_id);
279 if card.has_first_strike() || card.has_double_strike() {
280 return true;
281 }
282 }
283 for &(blocker_id, _) in &self.blockers {
284 if !game.card_is_in_zone(blocker_id, ZoneType::Battlefield) {
285 continue;
286 }
287 let card = game.card(blocker_id);
288 if card.has_first_strike() || card.has_double_strike() {
289 return true;
290 }
291 }
292 false
293 }
294
295 pub fn resolve_damage_step(
300 &self,
301 game: &mut GameState,
302 agents: &mut [Box<dyn PlayerAgent>],
303 first_strike_only: bool,
304 as_unblocked_choices: &HashSet<CardId>,
305 ) -> CombatDamageResolution {
306 if game.prevent_all_combat_damage {
308 return CombatDamageResolution {
309 events: Vec::new(),
310 counter_table: Default::default(),
311 };
312 }
313
314 let mut events = Vec::new();
315 let mut counter_table = crate::game_entity_counter_table::GameEntityCounterTable::default();
316 let mut blocker_damage_allocations: HashMap<(CardId, CardId), i32> = HashMap::new();
317 let mut computed_blocker_allocations: HashSet<CardId> = HashSet::new();
318 let life_at_step_start: Vec<i32> = game.players.iter().map(|p| p.life).collect();
321
322 for (attacker_id, defender) in self.attackers.clone() {
323 if !game.card_is_in_zone(attacker_id, ZoneType::Battlefield) {
325 continue;
326 }
327
328 let attacker = game.card(attacker_id);
329 if crate::staticability::static_ability_assign_no_combat_damage::assign_no_combat_damage(
330 &game.cards,
331 attacker,
332 ) {
333 continue;
334 }
335 let attacker_has_fs = attacker.has_first_strike();
336 let attacker_has_ds = attacker.has_double_strike();
337 let attacker_has_trample = attacker.has_trample();
338 let attacker_has_deathtouch = attacker.has_deathtouch();
339 let attacker_has_lifelink = attacker.has_lifelink();
340 let defending_player = defender.controlling_player(game);
341 let attacker_has_infect_for_player = attacker.has_infect()
342 || crate::staticability::static_ability_infect_damage::is_infect_damage_with_life_override(
343 game,
344 &game.cards,
345 defending_player,
346 attacker.controller,
347 life_at_step_start.get(defending_player.index()).copied(),
348 );
349 let attacker_has_infect_for_creature = attacker.has_infect();
350 let attacker_has_wither = attacker.has_wither()
351 || crate::staticability::static_ability_wither_damage::is_wither_damage(
352 &game.cards,
353 attacker,
354 );
355 let attacker_toxic_count = attacker.get_toxic_count();
356 let attacker_controller = attacker.controller;
357 let can_divide_damage_as_choose = attacker.has_keyword(
358 "You may assign CARDNAME's combat damage divided as you choose among defending player and/or any number of creatures they control.",
359 );
360 let can_assign_unblocked_to_creature = attacker.has_keyword(
361 "If CARDNAME is unblocked, you may have it assign its combat damage to a creature defending player controls.",
362 );
363 let has_trample_planeswalker = attacker.has_keyword("Trample:Planeswalker");
364
365 let attacker_deals_damage = if first_strike_only {
367 attacker_has_fs || attacker_has_ds
368 } else {
369 !attacker_has_fs || attacker_has_ds
371 };
372
373 let attacker_power = if crate::staticability::static_ability_combat_damage_toughness::combat_damage_uses_toughness(
374 &game.cards,
375 game.card(attacker_id),
376 ) {
377 game.card(attacker_id).toughness()
378 } else {
379 game.card(attacker_id).power()
380 };
381
382 let attacker_card = game.card(attacker_id);
383 let assign_as_unblocked =
384 crate::staticability::static_ability_assign_combat_damage_as_unblocked::has_mandatory_assign_as_unblocked(
385 &game.cards,
386 attacker_card,
387 )
388 || crate::staticability::static_ability_assign_combat_damage_as_unblocked::assign_as_unblocked(
389 &game.cards,
390 attacker_card,
391 as_unblocked_choices.contains(&attacker_id),
392 );
393
394 let attacker_was_blocked = self.was_blocked_this_combat(attacker_id);
395 let blockers = if assign_as_unblocked {
396 Vec::new()
397 } else if let Some(ordered) = self.damage_order.get(&attacker_id) {
398 ordered.clone()
400 } else {
401 self.get_blockers_for(attacker_id)
402 };
403
404 if blockers.is_empty() && !attacker_was_blocked {
405 if !attacker_deals_damage || attacker_power <= 0 {
407 continue;
408 }
409
410 let defending_creatures = defending_player_creatures(game, defender);
411 if can_divide_damage_as_choose
412 && !defending_creatures.is_empty()
413 && agents[attacker_controller.index()].confirm_action(
414 attacker_controller,
415 Some("AlternativeDamageAssignment"),
416 &format!(
417 "Assign {} combat damage divided as you choose among defending player and/or creatures they control?",
418 game.card(attacker_id).card_name
419 ),
420 &[],
421 Some(attacker_id),
422 None,
423 )
424 {
425 let assignments = agents[attacker_controller.index()].assign_combat_damage(
426 game,
427 attacker_controller,
428 attacker_id,
429 &defending_creatures,
430 Some(DefenderId::Player(defending_player)),
431 attacker_power,
432 );
433 let (to_creatures, to_player) = validate_damage_assignment(
434 game,
435 attacker_id,
436 &defending_creatures,
437 Some(DefenderId::Player(defending_player)),
438 attacker_power,
439 &assignments,
440 );
441
442 for &(target_id, dmg) in &to_creatures {
443 deal_combat_damage_to_card(
444 game,
445 attacker_id,
446 target_id,
447 dmg,
448 attacker_has_deathtouch,
449 attacker_has_lifelink,
450 attacker_controller,
451 attacker_has_wither || attacker_has_infect_for_creature,
452 Some(agents),
453 &mut counter_table,
454 );
455 events.push(CombatDamageEvent {
456 source: attacker_id,
457 target_player: None,
458 target_card: Some(target_id),
459 amount: dmg,
460 is_combat: true,
461 lifelink_player: if attacker_has_lifelink {
462 Some(attacker_controller)
463 } else {
464 None
465 },
466 lifelink_amount: if attacker_has_lifelink { dmg } else { 0 },
467 });
468 }
469 if to_player > 0 {
470 deal_combat_damage_to_player(
471 game,
472 attacker_id,
473 defending_player,
474 to_player,
475 attacker_has_lifelink,
476 attacker_controller,
477 attacker_has_infect_for_player,
478 attacker_toxic_count,
479 Some(agents),
480 &mut counter_table,
481 );
482 events.push(CombatDamageEvent {
483 source: attacker_id,
484 target_player: Some(defending_player),
485 target_card: None,
486 amount: to_player,
487 is_combat: true,
488 lifelink_player: if attacker_has_lifelink {
489 Some(attacker_controller)
490 } else {
491 None
492 },
493 lifelink_amount: if attacker_has_lifelink { to_player } else { 0 },
494 });
495 if game.player_is_commander(game.card(attacker_id).owner, attacker_id) {
496 game.player_add_commander_damage(
497 defending_player,
498 attacker_id,
499 to_player,
500 );
501 }
502 }
503 continue;
504 }
505
506 if can_assign_unblocked_to_creature
507 && !attacker_was_blocked
508 && !defending_creatures.is_empty()
509 && agents[attacker_controller.index()].confirm_action(
510 attacker_controller,
511 Some("AlternativeDamageAssignment"),
512 &format!(
513 "Assign {} combat damage to a creature defending player controls?",
514 game.card(attacker_id).card_name
515 ),
516 &[],
517 Some(attacker_id),
518 None,
519 )
520 {
521 if let Some(chosen) = agents[attacker_controller.index()].choose_target_card(
522 attacker_controller,
523 &defending_creatures,
524 None,
525 ) {
526 deal_combat_damage_to_card(
527 game,
528 attacker_id,
529 chosen,
530 attacker_power,
531 attacker_has_deathtouch,
532 attacker_has_lifelink,
533 attacker_controller,
534 attacker_has_wither || attacker_has_infect_for_creature,
535 Some(agents),
536 &mut counter_table,
537 );
538 events.push(CombatDamageEvent {
539 source: attacker_id,
540 target_player: None,
541 target_card: Some(chosen),
542 amount: attacker_power,
543 is_combat: true,
544 lifelink_player: if attacker_has_lifelink {
545 Some(attacker_controller)
546 } else {
547 None
548 },
549 lifelink_amount: if attacker_has_lifelink {
550 attacker_power
551 } else {
552 0
553 },
554 });
555 continue;
556 }
557 }
558 match defender {
559 DefenderId::Player(defending_player) => {
560 deal_combat_damage_to_player(
561 game,
562 attacker_id,
563 defending_player,
564 attacker_power,
565 attacker_has_lifelink,
566 attacker_controller,
567 attacker_has_infect_for_player,
568 attacker_toxic_count,
569 Some(agents),
570 &mut counter_table,
571 );
572 events.push(CombatDamageEvent {
573 source: attacker_id,
574 target_player: Some(defending_player),
575 target_card: None,
576 amount: attacker_power,
577 is_combat: true,
578 lifelink_player: if attacker_has_lifelink {
579 Some(attacker_controller)
580 } else {
581 None
582 },
583 lifelink_amount: if attacker_has_lifelink {
584 attacker_power
585 } else {
586 0
587 },
588 });
589 if game.player_is_commander(game.card(attacker_id).owner, attacker_id) {
591 game.player_add_commander_damage(
592 defending_player,
593 attacker_id,
594 attacker_power,
595 );
596 }
597 }
598 DefenderId::Permanent(target_id) => {
599 deal_combat_damage_to_card(
601 game,
602 attacker_id,
603 target_id,
604 attacker_power,
605 attacker_has_deathtouch,
606 attacker_has_lifelink,
607 attacker_controller,
608 attacker_has_wither || attacker_has_infect_for_creature,
609 Some(agents),
610 &mut counter_table,
611 );
612 events.push(CombatDamageEvent {
613 source: attacker_id,
614 target_player: None,
615 target_card: Some(target_id),
616 amount: attacker_power,
617 is_combat: true,
618 lifelink_player: if attacker_has_lifelink {
619 Some(attacker_controller)
620 } else {
621 None
622 },
623 lifelink_amount: if attacker_has_lifelink {
624 attacker_power
625 } else {
626 0
627 },
628 });
629 }
630 }
631 } else {
632 let remaining_damage = if attacker_deals_damage && attacker_power > 0 {
637 attacker_power
638 } else {
639 0
640 };
641 let mut alive_blockers: Vec<CardId> = blockers
645 .iter()
646 .copied()
647 .filter(|&bid| game.card_is_in_zone(bid, ZoneType::Battlefield))
648 .collect();
649 let mut effective_defender = defender;
650 if has_trample_planeswalker {
651 if let DefenderId::Permanent(target_id) = defender {
652 if !alive_blockers.contains(&target_id) {
653 alive_blockers.push(target_id);
654 }
655 effective_defender = DefenderId::Player(defending_player);
656 }
657 }
658
659 let defending_creatures = defending_player_creatures(game, effective_defender);
660 let use_divide_as_choose = can_divide_damage_as_choose
661 && !defending_creatures.is_empty()
662 && agents[attacker_controller.index()].confirm_action(
663 attacker_controller,
664 Some("AlternativeDamageAssignment"),
665 &format!(
666 "Assign {} combat damage divided as you choose among defending player and/or creatures they control?",
667 game.card(attacker_id).card_name
668 ),
669 &[],
670 Some(attacker_id),
671 None,
672 );
673 if use_divide_as_choose {
674 for cid in defending_creatures {
675 if !alive_blockers.contains(&cid) {
676 alive_blockers.push(cid);
677 }
678 }
679 }
680
681 let can_assign_to_defender = attacker_has_trample || use_divide_as_choose;
682 if alive_blockers.is_empty() && !can_assign_to_defender {
683 continue;
684 }
685 let must_prompt_assignment =
692 remaining_damage > 0 && (can_assign_to_defender || !alive_blockers.is_empty());
693
694 let assignments = if must_prompt_assignment {
695 let controller = game.card(attacker_id).controller;
696 let defender_for_prompt = if can_assign_to_defender {
697 Some(effective_defender)
698 } else {
699 None
700 };
701 agents[controller.index()].assign_combat_damage(
702 game,
703 controller,
704 attacker_id,
705 &alive_blockers,
706 defender_for_prompt,
707 remaining_damage,
708 )
709 } else if let Some(&only_blocker) = alive_blockers.first() {
710 vec![(Some(only_blocker), remaining_damage)]
711 } else if can_assign_to_defender {
712 vec![(None, remaining_damage)]
713 } else {
714 Vec::new()
715 };
716
717 let (damage_assignments, defender_damage) = validate_damage_assignment(
718 game,
719 attacker_id,
720 &alive_blockers,
721 can_assign_to_defender.then_some(effective_defender),
722 remaining_damage,
723 &assignments,
724 );
725
726 struct BlockerDamageInfo {
731 blocker_id: CardId,
732 power: i32,
733 has_deathtouch: bool,
734 has_lifelink: bool,
735 has_wither_or_infect: bool,
736 controller: PlayerId,
737 }
738 let mut blocker_damage_infos: Vec<BlockerDamageInfo> = Vec::new();
739 for &blocker_id in &blockers {
740 if !game.card_is_in_zone(blocker_id, ZoneType::Battlefield) {
741 continue;
742 }
743 let blocker_card = game.card(blocker_id);
744 if crate::staticability::static_ability_assign_no_combat_damage::assign_no_combat_damage(
745 &game.cards,
746 blocker_card,
747 ) {
748 continue;
749 }
750 let blocker_has_fs = blocker_card.has_first_strike();
751 let blocker_has_ds = blocker_card.has_double_strike();
752 let blocker_deals = if first_strike_only {
753 blocker_has_fs || blocker_has_ds
754 } else {
755 !blocker_has_fs || blocker_has_ds
756 };
757 if !blocker_deals {
758 continue;
759 }
760 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
761 &game.cards,
762 game.card(attacker_id),
763 game.card(blocker_id),
764 ) {
765 continue;
766 }
767 let blocker_power = if crate::staticability::static_ability_combat_damage_toughness::combat_damage_uses_toughness(
768 &game.cards,
769 game.card(blocker_id),
770 ) {
771 game.card(blocker_id).toughness()
772 } else {
773 game.card(blocker_id).power()
774 };
775 if blocker_power > 0 {
776 if !computed_blocker_allocations.contains(&blocker_id) {
777 let per_attacker = compute_blocker_damage_allocations(
778 self,
779 game,
780 agents,
781 first_strike_only,
782 blocker_id,
783 blocker_power,
784 );
785 for (target_attacker, dmg) in per_attacker {
786 blocker_damage_allocations
787 .insert((blocker_id, target_attacker), dmg);
788 }
789 computed_blocker_allocations.insert(blocker_id);
790 }
791 let assigned_to_this_attacker = blocker_damage_allocations
792 .get(&(blocker_id, attacker_id))
793 .copied()
794 .unwrap_or(0);
795 if assigned_to_this_attacker <= 0 {
796 continue;
797 }
798 let blocker_has_infect = blocker_card.has_infect();
799 let blocker_has_wither = blocker_card.has_wither()
800 || crate::staticability::static_ability_wither_damage::is_wither_damage(
801 &game.cards,
802 blocker_card,
803 );
804 blocker_damage_infos.push(BlockerDamageInfo {
805 blocker_id,
806 power: assigned_to_this_attacker,
807 has_deathtouch: blocker_card.has_deathtouch(),
808 has_lifelink: blocker_card.has_lifelink(),
809 has_wither_or_infect: blocker_has_wither || blocker_has_infect,
810 controller: blocker_card.controller,
811 });
812 }
813 }
814
815 for &(blocker_id, damage_to_blocker) in &damage_assignments {
818 deal_combat_damage_to_card(
819 game,
820 attacker_id,
821 blocker_id,
822 damage_to_blocker,
823 attacker_has_deathtouch,
824 attacker_has_lifelink,
825 attacker_controller,
826 attacker_has_wither || attacker_has_infect_for_creature,
827 Some(agents),
828 &mut counter_table,
829 );
830 events.push(CombatDamageEvent {
831 source: attacker_id,
832 target_player: None,
833 target_card: Some(blocker_id),
834 amount: damage_to_blocker,
835 is_combat: true,
836 lifelink_player: if attacker_has_lifelink {
837 Some(attacker_controller)
838 } else {
839 None
840 },
841 lifelink_amount: if attacker_has_lifelink {
842 damage_to_blocker
843 } else {
844 0
845 },
846 });
847 }
848
849 if defender_damage > 0 {
850 match effective_defender {
851 DefenderId::Player(defending_player) => {
852 deal_combat_damage_to_player(
853 game,
854 attacker_id,
855 defending_player,
856 defender_damage,
857 attacker_has_lifelink,
858 attacker_controller,
859 attacker_has_infect_for_player,
860 attacker_toxic_count,
861 None, &mut counter_table,
863 );
864 events.push(CombatDamageEvent {
865 source: attacker_id,
866 target_player: Some(defending_player),
867 target_card: None,
868 amount: defender_damage,
869 is_combat: true,
870 lifelink_player: if attacker_has_lifelink {
871 Some(attacker_controller)
872 } else {
873 None
874 },
875 lifelink_amount: if attacker_has_lifelink {
876 defender_damage
877 } else {
878 0
879 },
880 });
881 if game.card(attacker_id).is_commander {
882 game.player_add_commander_damage(
883 defending_player,
884 attacker_id,
885 defender_damage,
886 );
887 }
888 }
889 DefenderId::Permanent(target_id) => {
890 deal_combat_damage_to_card(
891 game,
892 attacker_id,
893 target_id,
894 defender_damage,
895 attacker_has_deathtouch,
896 attacker_has_lifelink,
897 attacker_controller,
898 attacker_has_wither || attacker_has_infect_for_creature,
899 Some(agents),
900 &mut counter_table,
901 );
902 events.push(CombatDamageEvent {
903 source: attacker_id,
904 target_player: None,
905 target_card: Some(target_id),
906 amount: defender_damage,
907 is_combat: true,
908 lifelink_player: if attacker_has_lifelink {
909 Some(attacker_controller)
910 } else {
911 None
912 },
913 lifelink_amount: if attacker_has_lifelink {
914 defender_damage
915 } else {
916 0
917 },
918 });
919 }
920 }
921 }
922
923 for info in &blocker_damage_infos {
924 if !game.card_is_in_zone(info.blocker_id, ZoneType::Battlefield) {
926 continue;
927 }
928 deal_combat_damage_to_card(
929 game,
930 info.blocker_id,
931 attacker_id,
932 info.power,
933 info.has_deathtouch,
934 info.has_lifelink,
935 info.controller,
936 info.has_wither_or_infect,
937 Some(agents),
938 &mut counter_table,
939 );
940 events.push(CombatDamageEvent {
941 source: info.blocker_id,
942 target_player: None,
943 target_card: Some(attacker_id),
944 amount: info.power,
945 is_combat: true,
946 lifelink_player: if info.has_lifelink {
947 Some(info.controller)
948 } else {
949 None
950 },
951 lifelink_amount: if info.has_lifelink { info.power } else { 0 },
952 });
953 }
954
955 }
958 }
959
960 CombatDamageResolution {
961 events,
962 counter_table,
963 }
964 }
965
966 pub fn init_constraints(&self, game: &GameState) -> attack_constraints::AttackConstraints {
971 let attacking_player = self
972 .attacking_player
973 .expect("init_constraints called without attacking player");
974 let possible_defenders = combat_util::get_possible_defenders(game, attacking_player);
975 attack_constraints::AttackConstraints::new(game, attacking_player, &possible_defenders)
976 }
977
978 pub fn end_combat(&mut self, game: &mut GameState) {
982 for card in game.cards.iter_mut() {
984 if card.zone == ZoneType::Battlefield {
985 card.damage_history.end_combat();
986 }
987 }
988
989 for &(attacker_id, _) in &self.attackers {
991 game.card_mut(attacker_id).clear_attacking_player();
992 }
993
994 self.clear();
995 }
996
997 pub fn clear_attackers(&mut self, game: &mut GameState) {
1000 let attacker_ids: Vec<CardId> = self.attackers.iter().map(|(a, _)| *a).collect();
1001 for attacker_id in attacker_ids {
1002 self.remove_from_combat(attacker_id, game);
1003 }
1004 }
1005
1006 pub fn add_attacker(&mut self, attacker: CardId, defender: DefenderId) {
1009 self.attackers.retain(|(a, _)| *a != attacker);
1011 self.attackers.push((attacker, defender));
1012 }
1013
1014 pub fn add_blocker(&mut self, attacker: CardId, blocker: CardId) {
1017 self.blockers.push((blocker, attacker));
1018 self.blocked_attackers.insert(attacker);
1019 if let Some(order) = self.damage_order.get_mut(&attacker) {
1021 if !order.contains(&blocker) {
1022 order.push(blocker);
1023 }
1024 }
1025 }
1026
1027 pub fn remove_block_assignment(&mut self, attacker: CardId, blocker: CardId) {
1030 self.blockers
1031 .retain(|&(b, a)| !(b == blocker && a == attacker));
1032 if !self.blockers.iter().any(|(b, _)| *b == blocker) {
1033 self.blocker_zone_timestamps.remove(&blocker);
1034 }
1035 }
1036
1037 pub fn undo_blocking_assignment(&mut self, blocker: CardId) {
1040 self.blockers.retain(|&(b, _)| b != blocker);
1041 self.blocker_zone_timestamps.remove(&blocker);
1042 }
1043
1044 pub fn order_blockers_for_damage_assignment(
1051 &mut self,
1052 _game: &GameState,
1053 _agents: &mut [Box<dyn PlayerAgent>],
1054 ) {
1055 let attacker_ids: Vec<CardId> = self.attackers.iter().map(|(a, _)| *a).collect();
1056 for attacker_id in attacker_ids {
1057 let blockers = self.get_blockers_for(attacker_id);
1058 if blockers.is_empty() {
1059 continue;
1060 }
1061 self.damage_order.insert(attacker_id, blockers);
1064 }
1065 }
1066
1067 pub fn add_blocker_to_damage_assignment_order(&mut self, attacker: CardId, blocker: CardId) {
1070 let order = self.damage_order.entry(attacker).or_default();
1071 if !order.contains(&blocker) {
1072 order.push(blocker);
1073 }
1074 }
1075
1076 pub fn order_attackers_for_damage_assignment(
1079 &mut self,
1080 _game: &GameState,
1081 _agents: &mut [Box<dyn PlayerAgent>],
1082 ) {
1083 }
1088
1089 pub fn unregister_attacker(&mut self, card: CardId) {
1092 self.damage_order.remove(&card);
1094
1095 for order in self.damage_order.values_mut() {
1097 order.retain(|&c| c != card);
1098 }
1099
1100 self.attackers.retain(|(a, _)| *a != card);
1102 self.attacker_zone_timestamps.remove(&card);
1103 self.blockers.retain(|(_, a)| *a != card);
1104 let blocker_ids: HashSet<CardId> = self.blockers.iter().map(|(b, _)| *b).collect();
1105 self.blocker_zone_timestamps
1106 .retain(|blocker_id, _| blocker_ids.contains(blocker_id));
1107 }
1108
1109 pub fn unregister_defender(&mut self, card: CardId) {
1112 for order in self.damage_order.values_mut() {
1114 order.retain(|&c| c != card);
1115 }
1116
1117 self.blockers.retain(|(b, _)| *b != card);
1119 self.blocker_zone_timestamps.remove(&card);
1120 }
1121
1122 pub fn remove_from_combat(&mut self, card: CardId, game: &mut GameState) {
1125 if self.attackers.iter().any(|(a, _)| *a == card) {
1127 self.unregister_attacker(card);
1128 game.card_mut(card).clear_attacking_player();
1129 return;
1130 }
1131
1132 if self.blockers.iter().any(|(b, _)| *b == card) {
1134 self.unregister_defender(card);
1135 }
1136 }
1137
1138 pub fn fire_triggers_for_unblocked_attackers(&mut self) -> Vec<(CardId, DefenderId)> {
1144 let mut unblocked = Vec::new();
1145
1146 for &(attacker_id, defender) in &self.attackers {
1147 let is_blocked = self.blockers.iter().any(|(_, a)| *a == attacker_id);
1148 if !is_blocked {
1149 unblocked.push((attacker_id, defender));
1150 }
1151 }
1152
1153 unblocked
1154 }
1155
1156 pub fn assign_combat_damage(
1159 &self,
1160 game: &mut GameState,
1161 agents: &mut [Box<dyn PlayerAgent>],
1162 first_strike_damage: bool,
1163 as_unblocked_choices: &HashSet<CardId>,
1164 ) -> CombatDamageResolution {
1165 self.resolve_damage_step(game, agents, first_strike_damage, as_unblocked_choices)
1166 }
1167
1168 pub fn deal_assigned_damage(&self, game: &mut GameState) {
1172 game.copy_last_state();
1176 }
1177
1178 pub fn get_attackers(&self) -> Vec<CardId> {
1180 self.attackers.iter().map(|(a, _)| *a).collect()
1181 }
1182
1183 pub fn get_all_blockers(&self) -> Vec<CardId> {
1185 let mut result = Vec::new();
1186 for &(b, _) in &self.blockers {
1187 if !result.contains(&b) {
1188 result.push(b);
1189 }
1190 }
1191 result
1192 }
1193
1194 pub fn get_defender_by_attacker(&self, attacker: CardId) -> Option<DefenderId> {
1196 self.attackers
1197 .iter()
1198 .find(|(a, _)| *a == attacker)
1199 .map(|(_, d)| *d)
1200 }
1201
1202 pub fn get_defender_player_by_attacker(
1205 &self,
1206 attacker: CardId,
1207 game: &GameState,
1208 ) -> Option<PlayerId> {
1209 self.get_defender_by_attacker(attacker)
1210 .map(|d| d.controlling_player(game))
1211 }
1212
1213 pub fn is_blocking(&self, blocker: CardId) -> bool {
1215 self.blockers.iter().any(|(b, _)| *b == blocker)
1216 }
1217
1218 pub fn is_blocking_attacker(&self, blocker: CardId, attacker: CardId) -> bool {
1220 self.blockers
1221 .iter()
1222 .any(|&(b, a)| b == blocker && a == attacker)
1223 }
1224
1225 pub fn is_unblocked(&self, attacker: CardId) -> bool {
1227 self.is_attacking(attacker) && !self.is_blocked(attacker)
1228 }
1229
1230 pub fn get_unblocked_attackers(&self) -> Vec<CardId> {
1232 self.attackers
1233 .iter()
1234 .filter(|(a, _)| !self.is_blocked(*a))
1235 .map(|(a, _)| *a)
1236 .collect()
1237 }
1238}
1239
1240fn validate_damage_assignment(
1241 game: &GameState,
1242 attacker_id: CardId,
1243 blockers_in_order: &[CardId],
1244 defender: Option<DefenderId>,
1245 total_damage: i32,
1246 assignments: &[(Option<CardId>, i32)],
1247) -> (Vec<(CardId, i32)>, i32) {
1248 if total_damage <= 0 {
1249 return (Vec::new(), 0);
1250 }
1251
1252 let mut per_blocker: HashMap<CardId, i32> = HashMap::new();
1253 let mut defender_damage = 0;
1254 let mut assigned_total = 0;
1255
1256 let mut invalid = false;
1257
1258 for &(assignee, amount) in assignments {
1259 if amount < 0 {
1260 invalid = true;
1261 break;
1262 }
1263 if amount == 0 {
1264 continue;
1265 }
1266 assigned_total += amount;
1267 match assignee {
1268 Some(blocker_id) => {
1269 if !blockers_in_order.contains(&blocker_id) {
1270 invalid = true;
1271 break;
1272 }
1273 *per_blocker.entry(blocker_id).or_insert(0) += amount;
1274 }
1275 None => {
1276 if defender.is_none() {
1277 invalid = true;
1278 break;
1279 }
1280 defender_damage += amount;
1281 }
1282 }
1283 }
1284
1285 if assigned_total != total_damage {
1286 invalid = true;
1287 }
1288
1289 let has_deathtouch = game.card(attacker_id).has_deathtouch();
1290 let mut can_move_to_next = true;
1291 for &blocker_id in blockers_in_order {
1292 if !game.card_is_in_zone(blocker_id, ZoneType::Battlefield) {
1293 continue;
1294 }
1295 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
1296 &game.cards,
1297 game.card(blocker_id),
1298 game.card(attacker_id),
1299 ) {
1300 continue;
1301 }
1302
1303 let assigned = per_blocker.get(&blocker_id).copied().unwrap_or(0);
1304 let lethal = if has_deathtouch {
1305 1
1306 } else if game.card(blocker_id).type_line.is_planeswalker() {
1307 game.card(blocker_id)
1308 .counter_count(&crate::card::CounterType::Loyalty)
1309 .max(0)
1310 } else {
1311 damage_needed_to_kill_for_assignment(game, blocker_id, attacker_id, assigned.max(1))
1312 };
1313
1314 if !can_move_to_next && assigned > 0 {
1315 invalid = true;
1316 break;
1317 }
1318 if assigned < lethal {
1319 can_move_to_next = false;
1320 }
1321 }
1322
1323 if defender_damage > 0 && !can_move_to_next {
1324 invalid = true;
1325 }
1326
1327 if invalid {
1328 return fallback_damage_assignment(
1329 game,
1330 attacker_id,
1331 blockers_in_order,
1332 defender,
1333 total_damage,
1334 );
1335 }
1336
1337 let mut ordered_blocker_assignments: Vec<(CardId, i32)> = Vec::new();
1338 for &blocker_id in blockers_in_order {
1339 if let Some(amount) = per_blocker.get(&blocker_id).copied() {
1340 if amount > 0 {
1341 ordered_blocker_assignments.push((blocker_id, amount));
1342 }
1343 }
1344 }
1345
1346 (ordered_blocker_assignments, defender_damage)
1347}
1348
1349fn fallback_damage_assignment(
1350 game: &GameState,
1351 attacker_id: CardId,
1352 blockers_in_order: &[CardId],
1353 defender: Option<DefenderId>,
1354 total_damage: i32,
1355) -> (Vec<(CardId, i32)>, i32) {
1356 if total_damage <= 0 {
1357 return (Vec::new(), 0);
1358 }
1359
1360 let mut assignments: Vec<(CardId, i32)> = Vec::new();
1361 let mut damage_left = total_damage;
1362 let has_deathtouch = game.card(attacker_id).has_deathtouch();
1363
1364 for &blocker_id in blockers_in_order {
1365 if damage_left <= 0 {
1366 break;
1367 }
1368 if !game.card_is_in_zone(blocker_id, ZoneType::Battlefield) {
1369 continue;
1370 }
1371 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
1372 &game.cards,
1373 game.card(blocker_id),
1374 game.card(attacker_id),
1375 ) {
1376 continue;
1377 }
1378
1379 let lethal = if has_deathtouch {
1380 1
1381 } else if game.card(blocker_id).type_line.is_planeswalker() {
1382 game.card(blocker_id)
1383 .counter_count(&crate::card::CounterType::Loyalty)
1384 .max(0)
1385 } else {
1386 damage_needed_to_kill_for_assignment(game, blocker_id, attacker_id, damage_left)
1387 };
1388 let assign = lethal.min(damage_left);
1389 if assign > 0 {
1390 assignments.push((blocker_id, assign));
1391 damage_left -= assign;
1392 }
1393 }
1394
1395 if damage_left > 0 {
1396 if defender.is_some() {
1397 return (assignments, damage_left);
1398 }
1399 if let Some((_, amount)) = assignments.last_mut() {
1400 *amount += damage_left;
1401 } else if let Some(&first) = blockers_in_order.first() {
1402 assignments.push((first, damage_left));
1403 }
1404 return (assignments, 0);
1405 }
1406
1407 (assignments, 0)
1408}
1409
1410fn damage_needed_to_kill_for_assignment(
1411 game: &GameState,
1412 target: CardId,
1413 source: CardId,
1414 max_damage: i32,
1415) -> i32 {
1416 if max_damage <= 0 {
1417 return 0;
1418 }
1419
1420 let target_card = game.card(target);
1421 let source_card = game.card(source);
1422 let mut kill_damage = (target_card.toughness() - target_card.damage).max(0);
1423
1424 if target_card.has_keyword("Indestructible")
1425 && !source_card.has_wither()
1426 && !source_card.has_infect()
1427 {
1428 return max_damage + 1;
1429 }
1430 if source_card.has_deathtouch() && target_card.is_creature() {
1431 kill_damage = 1;
1432 }
1433
1434 for damage in 1..=max_damage {
1435 let mut sim = game.clone();
1436 let mut event = crate::replacement::replacement_handler::ReplacementEvent::DamageToCard {
1437 target,
1438 amount: damage,
1439 source: Some(source),
1440 is_combat: true,
1441 };
1442 let _ = crate::replacement::replacement_handler::apply_replacements(&mut sim, &mut event);
1443 let final_damage = match event {
1444 crate::replacement::replacement_handler::ReplacementEvent::DamageToCard {
1445 amount,
1446 ..
1447 } => amount.max(0),
1448 _ => 0,
1449 };
1450 if final_damage >= kill_damage {
1451 return damage;
1452 }
1453 }
1454
1455 max_damage + 1
1456}
1457
1458fn defending_player_creatures(game: &GameState, defender: DefenderId) -> Vec<CardId> {
1459 let defending_player = defender.controlling_player(game);
1460 game.cards_in_zone(ZoneType::Battlefield, defending_player)
1461 .iter()
1462 .copied()
1463 .filter(|&cid| game.card(cid).is_creature())
1464 .collect()
1465}
1466
1467fn compute_blocker_damage_allocations(
1468 combat: &CombatState,
1469 game: &GameState,
1470 agents: &mut [Box<dyn PlayerAgent>],
1471 first_strike_only: bool,
1472 blocker_id: CardId,
1473 blocker_power: i32,
1474) -> Vec<(CardId, i32)> {
1475 if blocker_power <= 0 {
1476 return Vec::new();
1477 }
1478
1479 let blocker = game.card(blocker_id);
1480 let has_fs = blocker.has_first_strike();
1481 let has_ds = blocker.has_double_strike();
1482 let deals_this_step = if first_strike_only {
1483 has_fs || has_ds
1484 } else {
1485 !has_fs || has_ds
1486 };
1487 if !deals_this_step {
1488 return Vec::new();
1489 }
1490
1491 let attackers_for_blocker: Vec<CardId> = combat
1492 .get_attackers_for(blocker_id)
1493 .into_iter()
1494 .filter(|&aid| game.card_is_in_zone(aid, ZoneType::Battlefield))
1495 .collect();
1496 if attackers_for_blocker.is_empty() {
1497 return Vec::new();
1498 }
1499
1500 let controller = blocker.controller;
1507 let assignments = agents[controller.index()].assign_combat_damage(
1508 game,
1509 controller,
1510 blocker_id,
1511 &attackers_for_blocker,
1512 None,
1513 blocker_power,
1514 );
1515 let (per_attacker, _to_defender) = validate_damage_assignment(
1516 game,
1517 blocker_id,
1518 &attackers_for_blocker,
1519 None,
1520 blocker_power,
1521 &assignments,
1522 );
1523 per_attacker
1524}
1525
1526pub fn get_available_attackers(game: &GameState, player: PlayerId) -> Vec<CardId> {
1531 combat_util::get_available_attackers(game, player)
1532}
1533
1534pub fn get_possible_defenders(game: &GameState, attacking_player: PlayerId) -> Vec<DefenderId> {
1536 combat_util::get_possible_defenders(game, attacking_player)
1537}
1538
1539pub fn get_available_blockers(game: &GameState, player: PlayerId) -> Vec<CardId> {
1541 combat_util::get_available_blockers(game, player)
1542}
1543
1544pub fn can_creature_block(game: &GameState, blocker_id: CardId, attacker_id: CardId) -> bool {
1546 combat_util::can_creature_block(game, blocker_id, attacker_id)
1547}
1548
1549pub fn filter_legal_blockers(
1551 game: &GameState,
1552 attackers: &[CardId],
1553 blockers: &[CardId],
1554) -> Vec<CardId> {
1555 combat_util::filter_legal_blockers(game, attackers, blockers)
1556}
1557
1558fn deal_combat_damage_to_player(
1560 game: &mut GameState,
1561 source: CardId,
1562 target: PlayerId,
1563 amount: i32,
1564 lifelink: bool,
1565 source_controller: PlayerId,
1566 source_has_infect: bool,
1567 source_toxic_count: Option<i32>,
1568 agents: Option<&mut [Box<dyn PlayerAgent>]>,
1569 counter_table: &mut crate::game_entity_counter_table::GameEntityCounterTable,
1570) {
1571 if amount > 0 {
1572 if source_has_infect {
1573 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_player(
1575 &game.cards,
1576 target,
1577 &crate::card::CounterType::Poison,
1578 ) {
1579 counter_table.put(
1580 Some(source_controller),
1581 crate::agent::GameEntity::Player(target),
1582 crate::card::CounterType::Poison,
1583 amount,
1584 );
1585 }
1586 } else {
1587 let dealt = game.deal_damage_to_player_from_with_agents(
1588 target,
1589 amount,
1590 Some(source),
1591 true,
1592 agents,
1593 );
1594 game.record_player_damage_assignment(Some(source), Some(target), dealt, true);
1595 }
1596 if let Some(toxic) = source_toxic_count {
1598 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_player(
1599 &game.cards,
1600 target,
1601 &crate::card::CounterType::Poison,
1602 ) {
1603 counter_table.put(
1604 Some(source_controller),
1605 crate::agent::GameEntity::Player(target),
1606 crate::card::CounterType::Poison,
1607 toxic,
1608 );
1609 }
1610 }
1611 if lifelink
1612 && !crate::staticability::static_ability_cant_gain_lose_pay_life::cant_gain_life(
1613 game,
1614 source_controller,
1615 )
1616 {
1617 let mut gl_event =
1619 crate::replacement::replacement_handler::ReplacementEvent::GainLife {
1620 player: source_controller,
1621 amount,
1622 };
1623 let gl_result =
1624 crate::replacement::replacement_handler::apply_replacements(game, &mut gl_event);
1625 if gl_result != crate::replacement::ReplacementResult::Skipped
1626 && gl_result != crate::replacement::ReplacementResult::Replaced
1627 {
1628 let final_amount =
1629 if let crate::replacement::replacement_handler::ReplacementEvent::GainLife {
1630 amount: a,
1631 ..
1632 } = gl_event
1633 {
1634 a
1635 } else {
1636 amount
1637 };
1638 if final_amount > 0 {
1639 game.player_gain_life(source_controller, final_amount);
1640 game.player_add_team_life_gained(source_controller, final_amount);
1641 }
1642 }
1643 }
1644 game.card_mut(source).damage_history.register_damage(
1645 amount,
1646 true,
1647 Some(source),
1648 crate::card::card_damage_history::TrackedEntity::Player(target),
1649 );
1650 }
1651}
1652
1653fn deal_combat_damage_to_card(
1655 game: &mut GameState,
1656 source: CardId,
1657 target: CardId,
1658 amount: i32,
1659 deathtouch: bool,
1660 lifelink: bool,
1661 source_controller: PlayerId,
1662 source_has_wither_or_infect: bool,
1663 agents: Option<&mut [Box<dyn PlayerAgent>]>,
1664 counter_table: &mut crate::game_entity_counter_table::GameEntityCounterTable,
1665) {
1666 if amount > 0 {
1667 if crate::staticability::static_ability_colorless_damage_source::target_is_protected_from_source(
1668 &game.cards,
1669 game.card(target),
1670 game.card(source),
1671 ) {
1672 return;
1673 }
1674 if !game.card(target).damage_sources_this_turn.contains(&source) {
1676 game.card_mut(target).add_damage_source_this_turn(source);
1677 }
1678 if source_has_wither_or_infect {
1679 if !crate::staticability::static_ability_cant_put_counter::any_cant_put_counter_on_card(
1681 &game.cards,
1682 game.card(target),
1683 &crate::card::CounterType::M1M1,
1684 ) {
1685 counter_table.put(
1686 Some(source_controller),
1687 crate::agent::GameEntity::Card(target),
1688 crate::card::CounterType::M1M1,
1689 amount,
1690 );
1691 }
1692 } else {
1693 game.deal_damage_to_card_from_with_agents(target, amount, Some(source), true, agents);
1694 }
1695 if deathtouch {
1696 game.card_mut(target).mark_deathtouch_damage();
1697 }
1698 if lifelink
1699 && !crate::staticability::static_ability_cant_gain_lose_pay_life::cant_gain_life(
1700 game,
1701 source_controller,
1702 )
1703 {
1704 let mut gl_event =
1706 crate::replacement::replacement_handler::ReplacementEvent::GainLife {
1707 player: source_controller,
1708 amount,
1709 };
1710 let gl_result =
1711 crate::replacement::replacement_handler::apply_replacements(game, &mut gl_event);
1712 if gl_result != crate::replacement::ReplacementResult::Skipped
1713 && gl_result != crate::replacement::ReplacementResult::Replaced
1714 {
1715 let final_amount =
1716 if let crate::replacement::replacement_handler::ReplacementEvent::GainLife {
1717 amount: a,
1718 ..
1719 } = gl_event
1720 {
1721 a
1722 } else {
1723 amount
1724 };
1725 if final_amount > 0 {
1726 game.player_gain_life(source_controller, final_amount);
1727 game.player_add_team_life_gained(source_controller, final_amount);
1728 }
1729 }
1730 }
1731 game.card_mut(source).damage_history.register_damage(
1732 amount,
1733 true,
1734 Some(source),
1735 crate::card::card_damage_history::TrackedEntity::Card(target),
1736 );
1737 }
1738}
1739
1740#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1745pub enum LureType {
1746 None,
1748 MustBeBlockedIfAble,
1750 AllMustBlock,
1752}
1753
1754pub fn get_lure_type(card: &crate::card::Card) -> LureType {
1756 combat_util::get_lure_type(card)
1757}
1758
1759pub fn compute_must_block_targets(
1761 game: &GameState,
1762 combat: &CombatState,
1763 blocker_id: CardId,
1764) -> Vec<CardId> {
1765 combat_util::compute_must_block_targets(game, combat, blocker_id)
1766}
1767
1768pub fn validate_blocks(game: &GameState, combat: &CombatState) -> Vec<(CardId, CardId)> {
1770 combat_util::validate_blocks(game, combat)
1771}