Combat Damage
Overview
The Combat Damage step is the critical phase of combat where damage is assigned and dealt between attacking and blocking creatures, as well as to players and planeswalkers. In Commander, this step has additional significance due to the presence of Commander damage tracking and the multiplayer nature of the format.
This document outlines the implementation details, edge cases, and testing strategies for the Combat Damage step in our Commander engine.
Core Concepts
Combat Damage Flow
The Combat Damage step follows this general flow:
- First, the active player assigns combat damage from their attacking creatures
- Then, each defending player assigns combat damage from their blocking creatures
- All damage is dealt simultaneously
- State-based actions are checked, including:
- Creatures with lethal damage are destroyed
- Players with 0 or less life lose the game
- Players with 21+ commander damage from a single commander lose the game
- Combat damage triggers are placed on the stack
- Priority passes to players in turn order
First Strike and Double Strike
Combat damage may occur in two sub-steps when creatures with first strike or double strike are involved:
- First Strike Combat Damage - Damage from creatures with first strike or double strike
- Regular Combat Damage - Damage from all other creatures and second instance of damage from double strike creatures
Implementation Design
Data Structures
#![allow(unused)] fn main() { // Represents the damage assignment for a specific attacker or blocker struct DamageAssignment { source: Entity, // The entity dealing damage target: Entity, // The entity receiving damage amount: u32, // Amount of damage is_commander_damage: bool, // Whether this is commander damage is_combat_damage: bool, // Always true for combat damage step } // Component for tracking assigned damage #[derive(Component)] struct AssignedDamage { assignments: Vec<DamageAssignment>, } // System resource for managing the combat damage step struct CombatDamageSystem { first_strike_round_completed: bool, damage_assignments: HashMap<Entity, Vec<DamageAssignment>>, } }
Combat Damage System
#![allow(unused)] fn main() { fn combat_damage_system( mut commands: Commands, mut combat_system: ResMut<CombatSystem>, turn_manager: Res<TurnManager>, creature_query: Query<(Entity, &Creature, &Health, Option<&FirstStrike>, Option<&DoubleStrike>)>, attacker_query: Query<(Entity, &Attacking)>, blocker_query: Query<(Entity, &Blocking)>, player_query: Query<(Entity, &Player, &Health)>, mut commander_damage: Query<&mut CommanderDamage>, // Other system parameters ) { // Check if we need to process first strike damage let current_step = match turn_manager.current_phase { Phase::Combat(CombatStep::FirstStrike) => CombatStep::FirstStrike, Phase::Combat(CombatStep::CombatDamage) => CombatStep::CombatDamage, _ => return, // Not in a combat damage step }; // Determine which creatures deal damage in this step let (first_strike_creatures, regular_creatures) = creature_query .iter() .partition(|(_, _, _, first_strike, double_strike)| first_strike.is_some() || double_strike.is_some() ); // Process creatures that deal damage in this step let applicable_creatures = match current_step { CombatStep::FirstStrike => &first_strike_creatures, CombatStep::CombatDamage => { // In regular combat damage, double strike creatures deal damage again, // and creatures without first/double strike deal damage for the first time let mut creatures = Vec::new(); creatures.extend(regular_creatures); creatures.extend( first_strike_creatures .iter() .filter(|(_, _, _, _, double_strike)| double_strike.is_some()) ); &creatures } _ => return, }; // Assign and deal damage for (entity, creature, health, _, _) in applicable_creatures { // Determine damage amount and recipients let power = creature.power; if let Ok((_, attacking)) = attacker_query.get(*entity) { // Attacking creature deals damage assign_attacker_damage( *entity, power, attacking.defending, &blocker_query, &mut combat_system ); } else if let Ok((_, blocking)) = blocker_query.get(*entity) { // Blocking creature deals damage assign_blocker_damage( *entity, power, &blocking.blocked_attackers, &mut combat_system ); } } // Deal all assigned damage simultaneously process_damage_assignments(&mut commands, &combat_system, &mut commander_damage); // Check for state-based actions check_state_based_actions(&mut commands, &player_query, &creature_query); // If this was the first strike step, we'll continue to regular damage // If this was regular combat damage, we'll proceed to end of combat if current_step == CombatStep::FirstStrike { combat_system.first_strike_round_completed = true; } else { combat_system.first_strike_round_completed = false; // Clean up damage assignments combat_system.damage_assignments.clear(); } } }
Damage Assignment Functions
#![allow(unused)] fn main() { fn assign_attacker_damage( attacker: Entity, power: u32, defending: Entity, blocker_query: &Query<(Entity, &Blocking)>, combat_system: &mut ResMut<CombatSystem> ) { // Find blockers for this attacker let blockers = blocker_query .iter() .filter(|(_, blocking)| blocking.blocked_attackers.contains(&attacker)) .map(|(entity, _)| entity) .collect::<Vec<_>>(); if blockers.is_empty() { // Unblocked attacker - deal damage to player or planeswalker combat_system.damage_assignments.entry(attacker).or_default().push( DamageAssignment { source: attacker, target: defending, amount: power, is_commander_damage: true, // Check if attacker is a commander is_combat_damage: true, } ); } else { // Attacker is blocked - distribute damage among blockers // (In a real implementation, this would handle player choices for damage assignment) // Simplified version: divide damage evenly let damage_per_blocker = power / blockers.len() as u32; let remainder = power % blockers.len() as u32; for (i, blocker) in blockers.iter().enumerate() { let damage = damage_per_blocker + if i < remainder as usize { 1 } else { 0 }; combat_system.damage_assignments.entry(attacker).or_default().push( DamageAssignment { source: attacker, target: *blocker, amount: damage, is_commander_damage: false, // Commander damage only applies to players is_combat_damage: true, } ); } } } fn assign_blocker_damage( blocker: Entity, power: u32, blocked_attackers: &[Entity], combat_system: &mut ResMut<CombatSystem> ) { // Simplified version: divide damage evenly among attackers // (In a real implementation, this would handle player choices for damage assignment) let damage_per_attacker = power / blocked_attackers.len() as u32; let remainder = power % blocked_attackers.len() as u32; for (i, attacker) in blocked_attackers.iter().enumerate() { let damage = damage_per_attacker + if i < remainder as usize { 1 } else { 0 }; combat_system.damage_assignments.entry(blocker).or_default().push( DamageAssignment { source: blocker, target: *attacker, amount: damage, is_commander_damage: false, is_combat_damage: true, } ); } } fn process_damage_assignments( commands: &mut Commands, combat_system: &CombatSystem, commander_damage: &mut Query<&mut CommanderDamage> ) { // Process all damage assignments for (source, assignments) in &combat_system.damage_assignments { for assignment in assignments { // Deal damage to target commands.entity(assignment.target).insert( DamageReceived { amount: assignment.amount, source: assignment.source, is_combat_damage: true, } ); // If this is commander damage, update commander damage tracker if assignment.is_commander_damage { if let Ok(mut cmd_damage) = commander_damage.get_mut(assignment.target) { cmd_damage.add_damage(assignment.source, assignment.amount); } } // Add damage event for triggers commands.spawn(DamageEvent { source: assignment.source, target: assignment.target, amount: assignment.amount, is_combat_damage: true, }); } } } }
Special Cases and Edge Scenarios
Trample
When an attacking creature with trample is blocked, excess damage is dealt to the defending player:
#![allow(unused)] fn main() { fn assign_attacker_damage_with_trample( attacker: Entity, power: u32, defending: Entity, blocker_query: &Query<(Entity, &Blocking, &Health)>, trample_query: &Query<&Trample>, combat_system: &mut ResMut<CombatSystem> ) { // Find blockers for this attacker let blockers = blocker_query .iter() .filter(|(_, blocking, _)| blocking.blocked_attackers.contains(&attacker)) .collect::<Vec<_>>(); if blockers.is_empty() { // Handle unblocked attacker as normal // ... } else if trample_query.get(attacker).is_ok() { // Attacker has trample - assign lethal damage to each blocker let mut remaining_damage = power; for (blocker, _, health) in &blockers { let lethal_damage = health.current.min(remaining_damage); remaining_damage -= lethal_damage; // Assign lethal damage to blocker combat_system.damage_assignments.entry(attacker).or_default().push( DamageAssignment { source: attacker, target: *blocker, amount: lethal_damage, is_commander_damage: false, is_combat_damage: true, } ); } // Assign remaining damage to player if remaining_damage > 0 { combat_system.damage_assignments.entry(attacker).or_default().push( DamageAssignment { source: attacker, target: defending, amount: remaining_damage, is_commander_damage: true, // Check if attacker is a commander is_combat_damage: true, } ); } } else { // Handle normal blocked creature (no trample) // ... } } }
Deathtouch
Creatures with deathtouch need to assign only 1 damage to be considered lethal:
#![allow(unused)] fn main() { fn is_lethal_damage( amount: u32, source: Entity, deathtouch_query: &Query<&Deathtouch> ) -> bool { if amount > 0 && deathtouch_query.get(source).is_ok() { return true; } amount > 0 } }
Damage Prevention and Replacement
Damage can be prevented or modified by various effects:
#![allow(unused)] fn main() { fn apply_damage_prevention_effects( assignment: &mut DamageAssignment, prevention_query: &Query<&PreventDamage> ) { if let Ok(prevent) = prevention_query.get(assignment.target) { let prevented = prevent.amount.min(assignment.amount); assignment.amount -= prevented; } } }
Damage Redirection
Some effects can redirect damage to different entities:
#![allow(unused)] fn main() { fn apply_damage_redirection( assignment: &mut DamageAssignment, redirection_query: &Query<&RedirectDamage> ) { if let Ok(redirect) = redirection_query.get(assignment.target) { // Change the target of the damage assignment.target = redirect.new_target; // Adjust commander damage flag if needed assignment.is_commander_damage = is_commander_entity(redirect.new_target); } } }
Testing Strategy
Unit Tests
#![allow(unused)] fn main() { #[test] fn test_basic_combat_damage() { // Set up test world let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_systems(Update, combat_damage_system); // Create attacker and defender let attacker = app.world.spawn(( Creature { power: 3, toughness: 3, }, Attacking { defending: player_entity }, )).id(); let defender = app.world.spawn(( Player {}, Health { current: 20, maximum: 20 }, CommanderDamage::default(), )).id(); // Process combat damage app.update(); // Verify damage was dealt let health = app.world.get::<Health>(defender).unwrap(); assert_eq!(health.current, 17); } #[test] fn test_first_strike_damage() { // Set up test world with first strike creatures // ... } #[test] fn test_blocked_creature_damage() { // Test damage assignment with blockers // ... } #[test] fn test_commander_damage_tracking() { // Test commander damage accumulation // ... } }
Integration Tests
#![allow(unused)] fn main() { #[test] fn test_full_combat_sequence() { // Set up a complete combat sequence with attackers, blockers, // and damage calculation // ... } #[test] fn test_combat_with_damage_prevention() { // Test combat with damage prevention effects // ... } #[test] fn test_combat_with_replacement_effects() { // Test combat with damage replacement effects // ... } }
Edge Case Tests
#![allow(unused)] fn main() { #[test] fn test_damage_to_indestructible() { // Test damage to indestructible creatures // ... } #[test] fn test_damage_with_deathtouch_and_trample() { // Test the interaction of deathtouch and trample // ... } #[test] fn test_damage_redirection() { // Test redirection of combat damage // ... } }
Performance Considerations
-
Batch Damage Processing: Process all damage assignments simultaneously for better performance.
-
Damage Event Optimization: Use a more efficient event system for damage events to avoid spawning entities.
-
Damage Assignment Caching: Cache damage assignments to avoid recalculating them.
-
Parallel Processing: Use parallel processing for damage assignment when appropriate.
Conclusion
The Combat Damage step is a complex but crucial part of the Commander game engine. Proper implementation ensures fair and accurate damage calculation, especially for tracking commander damage which is a key victory condition in the format. By handling special abilities like first strike, double strike, trample, and deathtouch correctly, we create a robust combat system that faithfully represents the Magic: The Gathering rules while maintaining good performance.