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