1use super::*;
2use crate::card::card_damage_history::TrackedEntity;
3
4impl GameLoop {
5 pub fn step_combat(&mut self, game: &mut GameState, agents: &mut [Box<dyn PlayerAgent>]) {
6 let _perf_scope =
7 crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::Combat);
8 let active = game.active_player();
9 let defending = game.opponent_of(active);
10 self.combat.clear_with_cards(&mut game.cards);
11 game.turn.combat_block_assignments.clear();
12 self.combat.attacking_player = Some(active);
13 self.combat.defending_player = Some(defending);
14
15 self.set_phase(game, agents, PhaseType::CombatBegin);
17 self.emit_phase_trigger(game, PhaseType::CombatBegin);
18 self.step_with_priority(game, agents, false);
19 if game.game_over {
20 self.combat.clear_with_cards(&mut game.cards);
21 return;
22 }
23
24 if game.end_combat_requested {
26 game.end_combat_requested = false;
27 self.combat.clear_with_cards(&mut game.cards);
28 return;
29 }
30
31 apply_continuous_effects(game);
34 self.trigger_handler.reset_active_triggers(game);
35
36 game.copy_last_state();
39
40 game.stack.freeze_stack();
42 self.set_phase(game, agents, PhaseType::CombatDeclareAttackers);
43 let available_attackers = combat::get_available_attackers(game, active);
44 let possible_defenders = combat::get_possible_defenders(game, active);
45
46 let must_attackers = if available_attackers.is_empty() {
48 Vec::new()
49 } else {
50 let requirements = combat::attack_requirement::compute_attack_requirements(
51 &game.cards,
52 &available_attackers,
53 defending,
54 );
55 combat::attack_requirement::must_attack_ids(&requirements)
56 };
57
58 let pass_until_skip = must_attackers.is_empty()
59 && agents[active.index()]
60 .get_pass_until()
61 .is_some_and(|target| {
62 let reached = (active == target.player
63 && !game.turn.phase.is_before(target.phase))
64 || !game.player(target.player).is_alive();
65 !reached
66 });
67
68 let mut chosen_attackers: Vec<(CardId, combat::DefenderId)> = Vec::new();
71 if !available_attackers.is_empty() && !pass_until_skip {
72 let max_attempts = 5000;
77 for _attempt in 0..max_attempts {
78 agents[active.index()].snapshot_state(game, &self.mana_pools);
79 self.game_log.log(
80 GameLogEntryType::PriorityWaiting,
81 2,
82 format!(
83 "Waiting for {} attacker declaration",
84 game.player(active).name
85 ),
86 );
87 let agent = &mut agents[active.index()];
88 let mut picked =
89 agent.choose_attackers(active, &available_attackers, &possible_defenders);
90 if self.apply_pending_snapshot_restore(game, agents) {
91 return;
92 }
93 self.game_log.log(
94 GameLogEntryType::PriorityResponse,
95 2,
96 format!(
97 "{} declared {} attacker(s)",
98 game.player(active).name,
99 picked.len()
100 ),
101 );
102
103 let attacker_ids: Vec<CardId> = picked.iter().map(|(a, _)| *a).collect();
105 let illegal = combat::attack_restriction::validate_attack_restrictions(
106 &attacker_ids,
107 &game.cards,
108 );
109 if !illegal.is_empty() {
110 picked.retain(|(id, _)| !illegal.contains(id));
111 }
112
113 let global_max =
115 crate::staticability::static_ability_attack_restrict::global_attack_restrict(
116 &game.cards,
117 );
118
119 let mut invalid = false;
121 if let Some(max) = global_max {
122 if picked.len() > max as usize {
123 invalid = true;
124 }
125 }
126
127 if !invalid {
133 let current_violations = must_attackers
134 .iter()
135 .filter(|&&m| !picked.iter().any(|(a, _)| *a == m))
136 .count();
137 if current_violations > 0 {
138 let max_attackers = global_max.unwrap_or(i32::MAX) as usize;
143 let best_violations = must_attackers.len().saturating_sub(max_attackers);
144 if current_violations > best_violations {
145 invalid = true;
146 }
147 }
148 }
149
150 if !invalid {
154 let defender_max =
155 crate::staticability::static_ability_attack_restrict::attack_restrict_num_for_defender(
156 &game.cards,
157 defending,
158 );
159 if let Some(max) = defender_max {
160 let attackers_to_defender = picked
161 .iter()
162 .filter(|(_, def)| def.controlling_player(game) == defending)
163 .count();
164 if attackers_to_defender > max as usize {
165 invalid = true;
166 }
167 }
168 }
169
170 if invalid {
171 agents[active.index()].notify(
173 crate::agent::notification::GameNotification::Event(
174 crate::agent::GameLogEvent::warning("Attack declaration invalid"),
175 ),
176 );
177 continue;
178 }
179
180 chosen_attackers = picked;
181 break;
182 }
183 }
184
185 let premarked_attackers: Vec<(CardId, combat::DefenderId)> = chosen_attackers.clone();
190 for &(attacker_id, def) in &premarked_attackers {
191 let defending_player = def.controlling_player(game);
192 game.card_mut(attacker_id)
193 .set_attacking_player(defending_player);
194 if !game.card(attacker_id).has_vigilance() {
195 game.card_mut(attacker_id).set_tapped(true);
196 }
197 }
198
199 {
202 let declared_attackers: Vec<CardId> = chosen_attackers
203 .iter()
204 .map(|(attacker, _)| *attacker)
205 .collect();
206 let mut optional_exert_by_attacker: std::collections::HashMap<
207 CardId,
208 Vec<(i32, String)>,
209 > = std::collections::HashMap::new();
210 let mut optional_enlist_by_attacker: std::collections::HashMap<
211 CardId,
212 Vec<(i32, String)>,
213 > = std::collections::HashMap::new();
214
215 for &attacker in &declared_attackers {
216 let static_abilities = game.card(attacker).static_abilities.clone();
217 for st in &static_abilities {
218 if !st.check_mode(&crate::staticability::StaticMode::OptionalAttackCost) {
219 continue;
220 }
221 let Some(cost_raw) = st.ir.cost.as_deref() else {
222 continue;
223 };
224 let parsed = crate::cost::parse_cost(cost_raw);
225 for part in parsed.parts {
226 match part {
227 crate::cost::CostPart::Exert {
228 amount,
229 type_filter,
230 } => {
231 let amount_n = amount.resolve(game, attacker, active);
232 optional_exert_by_attacker
233 .entry(attacker)
234 .or_default()
235 .push((amount_n, type_filter));
236 }
237 crate::cost::CostPart::Enlist {
238 amount,
239 type_filter,
240 } => {
241 let amount_n = amount.resolve(game, attacker, active);
242 optional_enlist_by_attacker
243 .entry(attacker)
244 .or_default()
245 .push((amount_n, type_filter));
246 }
247 _ => {}
248 }
249 }
250 }
251 }
252
253 let possible_exerters: Vec<CardId> = declared_attackers
254 .iter()
255 .copied()
256 .filter(|cid| optional_exert_by_attacker.contains_key(cid))
257 .collect();
258 if !possible_exerters.is_empty() {
259 let chosen = agents[active.index()].exert_attackers(active, &possible_exerters);
260 for attacker in chosen {
261 if let Some(parts) = optional_exert_by_attacker.get(&attacker).cloned() {
264 for (resolved, type_filter) in parts {
265 if resolved > 0 {
266 self.pay_exert_cost(
267 game,
268 agents,
269 active,
270 attacker,
271 &type_filter,
272 resolved,
273 );
274 }
275 }
276 }
277 }
278 }
279
280 let enlist_can_pay = !crate::cost::get_enlist_targets(game, active).is_empty();
283 let possible_enlisters: Vec<CardId> = if enlist_can_pay {
284 declared_attackers
285 .iter()
286 .copied()
287 .filter(|cid| optional_enlist_by_attacker.contains_key(cid))
288 .collect()
289 } else {
290 Vec::new()
291 };
292
293 if !possible_enlisters.is_empty() {
294 let chosen = agents[active.index()].enlist_attackers(active, &possible_enlisters);
295 for attacker in chosen {
296 if let Some(parts) = optional_enlist_by_attacker.get(&attacker).cloned() {
297 for (resolved, type_filter) in parts {
298 if resolved > 0 {
299 self.pay_enlist_cost(
300 game,
301 agents,
302 active,
303 attacker,
304 &type_filter,
305 resolved,
306 );
307 }
308 }
309 }
310 }
311 }
312 }
313
314 {
316 let mut cost_failures = Vec::new();
317 for &(attacker_id, defender) in &chosen_attackers {
318 let cost = combat::attack_cost::get_attack_cost(
319 &game.cards,
320 game.card(attacker_id),
321 defender,
322 );
323 if cost > 0 {
324 let controller = game.card(attacker_id).controller;
325 let attacker_name = game.card(attacker_id).card_name.clone();
326 let description = format!("Pay {{{cost}}} to attack with {attacker_name}");
327
328 loop {
330 let tappable_lands = self.get_tappable_lands(game, controller);
331 let pool_snapshot = self.pool(controller).clone();
332 let untappable_lands =
333 self.get_untappable_lands(game, controller, &pool_snapshot);
334 let pool_total = self.pool(controller).total_mana();
335 let mana_payment_sources =
336 crate::mana::collect_mana_payment_sources(game, controller, &[]);
337
338 agents[controller.index()].snapshot_state(game, &self.mana_pools);
339 let action = agents[controller.index()].pay_combat_cost(
340 controller,
341 attacker_id,
342 cost,
343 &description,
344 &mana_payment_sources.mana_ability_options,
345 &tappable_lands,
346 &untappable_lands,
347 pool_total,
348 );
349
350 match action {
351 CombatCostAction::TapLand {
352 card_id: land_id,
353 mana_ability_index,
354 express_choice,
355 } => {
356 if !tappable_lands.contains(&land_id) {
357 continue;
358 }
359 let undo_record =
360 self.begin_mana_undo_action(game, controller, land_id);
361 let pool_snapshot = self.pool(controller).begin_tap_tracking();
362 let mana_ab = {
364 let c = game.card(land_id);
365 if let Some(requested_idx) = mana_ability_index {
366 c.activated_abilities
367 .iter()
368 .find(|ab| {
369 ab.is_mana_ability
370 && ab.ability_index == requested_idx
371 })
372 .cloned()
373 } else {
374 c.activated_abilities
375 .iter()
376 .find(|ab| ab.is_mana_ability)
377 .cloned()
378 }
379 };
380 if let Some(ab) = mana_ab {
381 self.with_shared_state_mutation(
382 game,
383 agents,
384 |this, game, agents| {
385 this.resolve_mana_ability(
386 game,
387 agents,
388 controller,
389 land_id,
390 &ab,
391 express_choice,
392 );
393 },
394 );
395 } else {
396 let atom_opt = {
397 let c = game.card(land_id);
398 if c.is_land() && !c.tapped {
399 basic_land_mana_atom(c)
400 } else {
401 None
402 }
403 };
404 if let Some(atom) = atom_opt {
405 game.tap(land_id);
406 self.pool_mut(controller).add(atom, 1);
407 self.trigger_handler.run_trigger(
408 TriggerType::Taps,
409 RunParams {
410 card: Some(land_id),
411 player: Some(controller),
412 ..Default::default()
413 },
414 false,
415 );
416 self.trigger_handler.run_trigger(
417 TriggerType::TapsForMana,
418 RunParams {
419 card: Some(land_id),
420 player: Some(controller),
421 ..Default::default()
422 },
423 false,
424 );
425 }
426 }
427 let produced =
428 self.pool(controller).end_tap_tracking(&pool_snapshot);
429 self.finish_mana_undo_action(undo_record, produced.len());
430 }
431 CombatCostAction::UntapLand(land_id) => {
432 if !untappable_lands.contains(&land_id) {
433 continue;
434 }
435 self.undo_mana_action(game, controller, land_id);
436 }
437 CombatCostAction::Pay => {
438 self.invalidate_mana_undo_for_player(controller);
439 let pool = &mut self.mana_pools[controller.index()];
440 if pool.total_mana() >= cost {
441 pool.spend_generic(cost);
442 } else {
444 cost_failures.push(attacker_id);
446 }
447 break;
448 }
449 CombatCostAction::Decline => {
450 self.invalidate_mana_undo_for_player(controller);
451 cost_failures.push(attacker_id);
452 break;
453 }
454 }
455 }
456 }
457 }
458 chosen_attackers.retain(|(id, _)| !cost_failures.contains(id));
459 }
460
461 for &(attacker_id, _) in &premarked_attackers {
463 if !chosen_attackers.iter().any(|(id, _)| *id == attacker_id) {
464 game.card_mut(attacker_id).clear_attacking_player();
465 if !game.card(attacker_id).has_vigilance() {
466 game.card_mut(attacker_id).set_tapped(false);
467 }
468 }
469 }
470
471 if !chosen_attackers.is_empty() {
472 crate::agent::notify_all_agents(
473 agents,
474 crate::agent::GameLogEvent::action("Combat phase begins").with_player(active),
475 );
476 let attackers_msg = chosen_attackers
477 .iter()
478 .map(|(attacker_id, defender)| {
479 let attacker_name = game.card(*attacker_id).card_name.clone();
480 let defender_name = match defender {
481 combat::DefenderId::Player(pid) => game.player(*pid).name.clone(),
482 combat::DefenderId::Permanent(cid) => game.card(*cid).card_name.clone(),
483 };
484 format!("{attacker_name} -> {defender_name}")
485 })
486 .collect::<Vec<_>>()
487 .join(", ");
488 crate::agent::notify_all_agents(
489 agents,
490 crate::agent::GameLogEvent::action(format!("Attackers: {attackers_msg}"))
491 .with_player(active),
492 );
493 }
494
495 let num_attackers = chosen_attackers.len() as i32;
497 game.player_attack_combat_reset(active);
498 for &(attacker_id, defender) in &chosen_attackers {
499 if !game.card(attacker_id).has_vigilance() {
500 if game.card(attacker_id).tapped {
504 game.untap(attacker_id);
505 }
506 game.tap(attacker_id);
507 self.trigger_handler.run_trigger(
510 TriggerType::Taps,
511 RunParams {
512 card: Some(attacker_id),
513 player: Some(active),
514 ..Default::default()
515 },
516 false,
517 );
518 }
519 game.card_mut(attacker_id).mark_attacked_this_turn();
520 let def_player = defender.controlling_player(game);
522 game.card_mut(attacker_id).set_attacking_player(def_player);
523 self.combat.declare_attacker(
524 attacker_id,
525 defender,
526 game.card(attacker_id).zone_timestamp,
527 );
528
529 game.card_mut(attacker_id)
531 .damage_history
532 .record_attack(num_attackers - 1);
533 game.card_mut(attacker_id)
534 .damage_history
535 .set_creature_attacked_this_combat(
536 Some(match defender {
537 combat::DefenderId::Player(pid) => TrackedEntity::Player(pid),
538 combat::DefenderId::Permanent(cid) => TrackedEntity::Card(cid),
539 }),
540 num_attackers - 1,
541 matches!(defender, combat::DefenderId::Permanent(_)),
542 );
543 if let combat::DefenderId::Player(pid) = defender {
544 if !game
545 .player(active)
546 .attacked_players_this_turn
547 .contains(&pid)
548 {
549 game.player_record_attacked_player(active, pid);
550 }
551 if !game
552 .player(active)
553 .attacked_players_this_combat
554 .contains(&pid)
555 {
556 game.player_record_attacked_player(active, pid);
557 }
558 }
559
560 crate::ability::effects::ring_tempts_you_effect::sync_ring_effect(
561 game,
562 &mut self.trigger_handler,
563 active,
564 );
565
566 self.trigger_handler.run_trigger(
568 TriggerType::Attacks,
569 RunParams {
570 attacker: Some(attacker_id),
571 card: Some(attacker_id),
572 defending_player: Some(def_player),
573 num_attackers: Some(num_attackers as usize),
574 ..Default::default()
575 },
576 false,
577 );
578 }
579 if !chosen_attackers.is_empty() {
581 let mut grouped_attackers: std::collections::HashMap<combat::DefenderId, Vec<CardId>> =
582 std::collections::HashMap::new();
583 for &(attacker_id, defender) in &chosen_attackers {
584 grouped_attackers
585 .entry(defender)
586 .or_default()
587 .push(attacker_id);
588 }
589 let mut attacked_player_ids = Vec::new();
590 let mut attacked_card_ids = Vec::new();
591 for (defender, attackers) in &grouped_attackers {
592 let mut params = RunParams {
593 attacker_ids: Some(attackers.clone()),
594 player: Some(game.active_player()),
595 attacking_player: Some(game.active_player()),
596 ..Default::default()
597 };
598 match defender {
599 combat::DefenderId::Player(pid) => {
600 params.attacked_player = Some(*pid);
601 params.defenders_player_ids = Some(vec![*pid]);
602 attacked_player_ids.push(*pid);
603 }
604 combat::DefenderId::Permanent(cid) => {
605 params.attacked_card = Some(*cid);
606 params.defenders_card_ids = Some(vec![*cid]);
607 attacked_card_ids.push(*cid);
608 }
609 }
610 self.trigger_handler.run_trigger(
611 TriggerType::AttackersDeclaredOneTarget,
612 params,
613 false,
614 );
615 }
616
617 let attacker_ids: Vec<CardId> = chosen_attackers.iter().map(|(a, _)| *a).collect();
618 self.trigger_handler.run_trigger(
619 TriggerType::AttackersDeclared,
620 RunParams {
621 player: Some(game.active_player()),
622 attacking_player: Some(game.active_player()),
623 attacker_ids: Some(attacker_ids),
624 defenders_player_ids: if attacked_player_ids.is_empty() {
625 None
626 } else {
627 Some(attacked_player_ids)
628 },
629 defenders_card_ids: if attacked_card_ids.is_empty() {
630 None
631 } else {
632 Some(attacked_card_ids)
633 },
634 ..Default::default()
635 },
636 false,
637 );
638 }
639 apply_continuous_effects(game);
643 self.trigger_handler.reset_active_triggers(game);
644 game.stack.unfreeze_stack();
646 self.step_with_priority(game, agents, false);
651 if game.game_over {
652 self.combat.clear_with_cards(&mut game.cards);
653 return;
654 }
655
656 self.combat.remove_absent_combatants(&game.cards);
659 if !self.combat.has_attackers() {
660 self.set_phase(game, agents, PhaseType::CombatDeclareBlockers);
661 }
662 if self.combat.has_attackers() {
663 {
665 use crate::replacement::replacement_handler::{
666 apply_replacements, ReplacementEvent,
667 };
668 use crate::replacement::ReplacementResult;
669 let mut event = ReplacementEvent::DeclareBlocker { player: defending };
670 let result = apply_replacements(game, &mut event);
671 if result == ReplacementResult::Skipped || result == ReplacementResult::Replaced {
672 }
674 }
675
676 game.stack.freeze_stack();
678 self.set_phase(game, agents, PhaseType::CombatDeclareBlockers);
679 let attacker_card_ids: Vec<CardId> =
680 self.combat.attackers.iter().map(|(a, _)| *a).collect();
681 let available_blockers = combat::get_available_blockers(game, defending);
682 let legal_blockers =
683 combat::filter_legal_blockers(game, &attacker_card_ids, &available_blockers);
684 let has_any_legal_blocker = !legal_blockers.is_empty();
685
686 if has_any_legal_blocker {
687 agents[defending.index()].snapshot_state(game, &self.mana_pools);
688 self.game_log.log(
689 GameLogEntryType::PriorityWaiting,
690 2,
691 format!(
692 "Waiting for {} blocker declaration",
693 game.player(defending).name
694 ),
695 );
696 let max_blockers = {
697 let raw =
698 crate::staticability::static_ability_block_restrict::block_restrict_num(
699 &game.cards,
700 defending,
701 );
702 if raw < i32::MAX {
703 Some(raw as usize)
704 } else {
705 None
706 }
707 };
708 let mut chosen_blockers = {
709 let def_agent = &mut agents[defending.index()];
710 def_agent.choose_blockers(
711 defending,
712 &attacker_card_ids,
713 &available_blockers,
714 max_blockers,
715 )
716 };
717 if self.apply_pending_snapshot_restore(game, agents) {
718 return;
719 }
720 let mut seen_blockers = std::collections::HashSet::new();
722 chosen_blockers.retain(|(blocker, _)| seen_blockers.insert(*blocker));
723 self.game_log.log(
724 GameLogEntryType::PriorityResponse,
725 2,
726 format!(
727 "{} declared {} blocker assignment(s)",
728 game.player(defending).name,
729 chosen_blockers.len()
730 ),
731 );
732
733 for (blocker, attacker) in chosen_blockers.into_iter() {
734 if !combat::can_creature_block(game, blocker, attacker) {
736 continue; }
738 self.combat.declare_blocker(
739 blocker,
740 attacker,
741 game.card(blocker).zone_timestamp,
742 );
743
744 self.trigger_handler.run_trigger(
746 TriggerType::Blocks,
747 RunParams {
748 blocker: Some(blocker),
749 blocked_attacker: Some(attacker),
750 card: Some(blocker),
751 ..Default::default()
752 },
753 false,
754 );
755 }
756
757 {
759 let mut block_cost_failures = Vec::new();
760 for &(blocker_id, attacker_id) in &self.combat.blockers {
761 let cost = combat::block_cost::get_block_cost(
762 &game.cards,
763 game.card(blocker_id),
764 game.card(attacker_id),
765 );
766 if cost > 0 {
767 let controller = game.card(blocker_id).controller;
768 let pool = &mut self.mana_pools[controller.index()];
769 if pool.total_mana() >= cost {
770 pool.spend_generic(cost);
771 } else {
772 block_cost_failures.push(blocker_id);
773 }
774 }
775 }
776 self.combat
777 .blockers
778 .retain(|(b, _)| !block_cost_failures.contains(b));
779 }
780
781 let invalid_blocks = combat::validate_blocks(game, &self.combat);
783 for (blocker_id, attacker_id) in &invalid_blocks {
784 self.combat
785 .blockers
786 .retain(|(b, a)| !(b == blocker_id && a == attacker_id));
787 }
788
789 let all_legal_blockers: Vec<CardId> = available_blockers.clone();
791 for &blocker_id in &all_legal_blockers {
792 let must_targets =
793 combat::compute_must_block_targets(game, &self.combat, blocker_id);
794 if must_targets.is_empty() {
795 continue;
796 }
797 let currently_blocking: Vec<CardId> = self
798 .combat
799 .blockers
800 .iter()
801 .filter(|(b, _)| *b == blocker_id)
802 .map(|(_, a)| *a)
803 .collect();
804 if !must_targets.iter().any(|t| currently_blocking.contains(t)) {
805 if combat::can_creature_block(game, blocker_id, must_targets[0]) {
807 self.combat.declare_blocker(
808 blocker_id,
809 must_targets[0],
810 game.card(blocker_id).zone_timestamp,
811 );
812 }
813 }
814 }
815
816 for &(blocker_id, attacker_id) in &self.combat.blockers {
818 game.card_mut(blocker_id).damage_history.record_block();
819 game.card_mut(attacker_id)
820 .damage_history
821 .record_got_blocked();
822 }
823
824 game.turn.combat_block_assignments = self.combat.blockers.clone();
826
827 if !self.combat.blockers.is_empty() {
828 let blockers_msg = self
829 .combat
830 .blockers
831 .iter()
832 .map(|(blocker_id, attacker_id)| {
833 let blocker_name = game.card(*blocker_id).card_name.clone();
834 let attacker_name = game.card(*attacker_id).card_name.clone();
835 format!("{blocker_name} -> {attacker_name}")
836 })
837 .collect::<Vec<_>>()
838 .join(", ");
839 crate::agent::notify_all_agents(
840 agents,
841 crate::agent::GameLogEvent::action(format!("Blockers: {blockers_msg}"))
842 .with_player(defending),
843 );
844 }
845 }
846
847 for &(attacker_id, _) in &self.combat.attackers.clone() {
852 let blockers_for = self.combat.get_blockers_for(attacker_id);
853 if blockers_for.len() > 1 {
854 self.combat.damage_order.insert(attacker_id, blockers_for);
855 }
856 }
857
858 game.stack.unfreeze_stack();
860
861 self.trigger_handler.run_trigger(
867 TriggerType::BlockersDeclared,
868 RunParams {
869 blocker_ids: Some(self.combat.blockers.iter().map(|(b, _)| *b).collect()),
870 ..Default::default()
871 },
872 false,
873 );
874
875 for &(attacker_id, defender_id) in &self.combat.attackers.clone() {
877 if self.combat.is_blocked(attacker_id) {
878 let blockers_for = self.combat.get_blockers_for(attacker_id);
879 self.trigger_handler.run_trigger(
880 TriggerType::AttackerBlocked,
881 RunParams {
882 attacker: Some(attacker_id),
883 card: Some(attacker_id),
884 defending_player: Some(defender_id.controlling_player(game)),
885 ..Default::default()
886 },
887 false,
888 );
889 self.trigger_handler.run_trigger(
890 TriggerType::AttackerBlockedOnce,
891 RunParams {
892 attacker: Some(attacker_id),
893 card: Some(attacker_id),
894 blocker_ids: Some(blockers_for.clone()),
895 defending_player: Some(defender_id.controlling_player(game)),
896 ..Default::default()
897 },
898 false,
899 );
900 for blocker_id in blockers_for {
901 self.trigger_handler.run_trigger(
902 TriggerType::AttackerBlockedByCreature,
903 RunParams {
904 attacker: Some(attacker_id),
905 card: Some(attacker_id),
906 blocker: Some(blocker_id),
907 blocked_attacker: Some(attacker_id),
908 defending_player: Some(defender_id.controlling_player(game)),
909 ..Default::default()
910 },
911 false,
912 );
913 }
914 } else {
915 self.trigger_handler.run_trigger(
916 TriggerType::AttackerUnblocked,
917 RunParams {
918 attacker: Some(attacker_id),
919 card: Some(attacker_id),
920 ..Default::default()
921 },
922 false,
923 );
924 self.trigger_handler.run_trigger(
925 TriggerType::AttackerUnblockedOnce,
926 RunParams {
927 attacker: Some(attacker_id),
928 card: Some(attacker_id),
929 ..Default::default()
930 },
931 false,
932 );
933 }
934 }
935
936 self.step_with_priority(game, agents, false);
937 if game.game_over {
938 self.combat.clear_with_cards(&mut game.cards);
939 game.turn.combat_block_assignments.clear();
940 return;
941 }
942 }
943
944 self.combat.remove_absent_combatants(&game.cards);
947
948 for &(attacker_id, _) in &self.combat.attackers.clone() {
951 self.combat.save_lki(attacker_id);
952 }
953 for &(blocker_id, _) in &self.combat.blockers.clone() {
954 self.combat.save_lki(blocker_id);
955 }
956
957 self.set_phase(game, agents, PhaseType::CombatFirstStrikeDamage);
958 self.combat.remove_absent_combatants(&game.cards);
959 if self.combat.has_attackers() {
960 game.copy_last_state();
963
964 let fs_unblocked_choices = self.choose_assign_as_unblocked(game, agents, true);
965 let combat::CombatDamageResolution {
966 events: fs_events,
967 counter_table: fs_counter_table,
968 } = self
969 .combat
970 .resolve_damage_step(game, agents, true, &fs_unblocked_choices);
971 for event in &fs_events {
973 if event.target_player.is_some() && event.amount > 0 {
974 game.card_mut(event.source)
975 .damage_history
976 .record_damage(event.amount, true);
977 }
978 }
979 let fs_damage_assigned = !fs_events.is_empty();
980 self.fire_combat_damage_triggers(&fs_events);
981 fs_counter_table.replace_counter_effect(
982 game,
983 Some(&mut self.trigger_handler),
984 Some(agents),
985 None,
986 false,
987 Default::default(),
988 );
989 self.trigger_handler.flush_waiting_triggers(game);
992 if fs_damage_assigned {
994 self.step_with_priority(game, agents, false);
995 }
996 if game.game_over {
997 self.combat.clear_with_cards(&mut game.cards);
998 game.turn.combat_block_assignments.clear();
999 return;
1000 }
1001 }
1002
1003 self.set_phase(game, agents, PhaseType::CombatDamage);
1004 self.combat.remove_absent_combatants(&game.cards);
1005 if self.combat.has_attackers() {
1006 {
1008 use crate::replacement::replacement_handler::{
1009 apply_replacements, ReplacementEvent,
1010 };
1011 let attacker_ids: Vec<CardId> =
1012 self.combat.attackers.iter().map(|(a, _)| *a).collect();
1013 for &attacker_id in &attacker_ids {
1014 let mut event = ReplacementEvent::AssignDealDamage { card: attacker_id };
1015 apply_replacements(game, &mut event);
1016 }
1017 }
1018
1019 game.copy_last_state();
1022
1023 let unblocked_choices = self.choose_assign_as_unblocked(game, agents, false);
1024 let combat::CombatDamageResolution {
1025 events: dmg_events,
1026 counter_table: damage_counter_table,
1027 } = self
1028 .combat
1029 .resolve_damage_step(game, agents, false, &unblocked_choices);
1030 for event in &dmg_events {
1032 if event.target_player.is_some() && event.amount > 0 {
1033 game.card_mut(event.source)
1034 .damage_history
1035 .record_damage(event.amount, true);
1036 }
1037 }
1038 let damage_assigned = !dmg_events.is_empty();
1042 if damage_assigned {
1043 self.notify_state_changed(game, agents);
1044 }
1045 self.fire_combat_damage_triggers(&dmg_events);
1046 damage_counter_table.replace_counter_effect(
1047 game,
1048 Some(&mut self.trigger_handler),
1049 Some(agents),
1050 None,
1051 false,
1052 Default::default(),
1053 );
1054 self.trigger_handler.flush_waiting_triggers(game);
1057 if damage_assigned {
1058 self.step_with_priority(game, agents, false);
1059 }
1060 if game.game_over {
1061 self.combat.clear_with_cards(&mut game.cards);
1062 game.turn.combat_block_assignments.clear();
1063 return;
1064 }
1065 }
1066
1067 self.set_phase(game, agents, PhaseType::CombatEnd);
1069 self.emit_phase_trigger(game, PhaseType::CombatEnd);
1070 crate::ability::effects::control_gain_effect::end_of_combat_hook(game);
1073 self.step_with_priority(game, agents, false);
1074
1075 for card in game.cards.iter_mut() {
1077 if card.zone == ZoneType::Battlefield && card.is_creature() {
1078 card.damage_history.end_combat();
1079 card.must_block = false;
1080 card.must_block_cards.clear();
1081 }
1082 }
1083
1084 self.combat.clear_with_cards(&mut game.cards);
1085 game.turn.combat_block_assignments.clear();
1086 apply_continuous_effects(game);
1091 self.trigger_handler.reset_active_triggers(game);
1092 }
1093
1094 fn choose_assign_as_unblocked(
1095 &mut self,
1096 game: &GameState,
1097 agents: &mut [Box<dyn PlayerAgent>],
1098 first_strike_only: bool,
1099 ) -> std::collections::HashSet<CardId> {
1100 let mut choices = std::collections::HashSet::new();
1101 for &(attacker_id, _) in &self.combat.attackers {
1102 if !self.combat.is_blocked(attacker_id) {
1103 continue;
1104 }
1105 let attacker = game.card(attacker_id);
1106 let has_fs = attacker.has_first_strike();
1107 let has_ds = attacker.has_double_strike();
1108 let deals_in_step = if first_strike_only {
1109 has_fs || has_ds
1110 } else {
1111 !has_fs || has_ds
1112 };
1113 if !deals_in_step {
1114 continue;
1115 }
1116 if !crate::staticability::static_ability_assign_combat_damage_as_unblocked::has_optional_assign_as_unblocked(
1117 &game.cards,
1118 attacker,
1119 ) {
1120 continue;
1121 }
1122
1123 let controller = attacker.controller;
1124 let desc = format!(
1125 "Have {} assign combat damage as though unblocked?",
1126 attacker.card_name
1127 );
1128 agents[controller.index()].snapshot_state(game, &self.mana_pools);
1129 if agents[controller.index()].choose_optional_trigger(
1130 controller,
1131 &desc,
1132 Some(attacker_id),
1133 None,
1134 ) {
1135 choices.insert(attacker_id);
1136 }
1137 }
1138 choices
1139 }
1140}