Combat Damage Calculation
Overview
Combat damage calculation is a critical aspect of the Magic: The Gathering combat system. This document explains how damage is calculated, assigned, and applied during combat in Rummage's implementation.
Damage Assignment
Basic Rules
- Each attacking or blocking creature assigns combat damage equal to its power
- Attacking creatures that aren't blocked assign their damage to the player or planeswalker they're attacking
- Blocking creatures assign their damage to the creature they're blocking
- Blocked creatures assign their damage to the creatures blocking them
Multiple Blockers
When multiple creatures block a single attacker:
- The attacking player puts the blocking creatures in a damage assignment order
- The attacker must assign at least lethal damage to each creature in the order before assigning damage to the next creature
- "Lethal damage" is damage equal to the creature's toughness minus damage already marked on it
#![allow(unused)] fn main() { pub fn determine_damage_assignment_order( mut commands: Commands, mut combat_state: ResMut<CombatState>, blocker_groups: Query<(&Creature, &Health, &Blocking)>, ) { for (attacker_entity, blockers) in combat_state.blockers_per_attacker.iter() { if blockers.len() > 1 { // Request damage assignment order from attacking player // Store the order in the combat state // ... } } } }
Damage Application
Once damage is assigned, it is applied simultaneously:
- Damage to creatures is marked on them but doesn't immediately reduce toughness
- Damage to players reduces their life total
- Damage to planeswalkers removes loyalty counters
- Special abilities like deathtouch and lifelink take effect at this time
#![allow(unused)] fn main() { pub fn apply_combat_damage( combat_state: Res<CombatState>, mut creatures: Query<(Entity, &Creature, &mut Health)>, mut players: Query<(Entity, &Player, &mut Life)>, mut planeswalkers: Query<(Entity, &Planeswalker, &mut Loyalty)>, mut damage_events: EventWriter<DamageEvent>, ) { // Calculate and apply damage from all sources // ... // For creatures for (target, damage_amount) in creature_damage.iter() { if let Ok((_, _, mut health)) = creatures.get_mut(*target) { health.marked_damage += *damage_amount; // Generate damage event damage_events.send(DamageEvent { source: source_entity, target: *target, amount: *damage_amount, is_combat_damage: true, }); } } // For players and planeswalkers (similar logic) // ... } }
Special Damage Considerations
Deathtouch
Deathtouch modifies damage assignment rules:
- Any amount of damage from a source with deathtouch is considered lethal damage
- When assigning damage from an attacker with deathtouch to multiple blockers, only 1 damage needs to be assigned to each creature before moving to the next
Trample
Trample allows excess damage to be assigned to the player or planeswalker:
- The attacker must still assign lethal damage to all blockers
- Any remaining damage can be assigned to the defending player or planeswalker
- If all blocking creatures are removed from combat, all damage is assigned to the player or planeswalker
#![allow(unused)] fn main() { pub fn handle_trample_damage( combat_state: Res<CombatState>, creatures: Query<(Entity, &Creature, &Attack, Option<&Trample>)>, blockers: Query<(Entity, &Creature, &Health, &Blocking)>, mut player_damage: ResMut<HashMap<Entity, u32>>, ) { for (attacker, creature, attack, trample) in creatures.iter() { if trample.is_some() { // Calculate how much damage is needed for blockers // Assign excess to the player or planeswalker // ... } } } }
Protection and Prevention
Some effects modify how damage is dealt or received:
- Protection prevents damage from sources with specific characteristics
- Prevention effects can replace some or all damage that would be dealt
- These effects are applied during damage calculation, before damage is actually dealt
Damage Resolution
After combat damage is dealt, the game checks for state-based actions:
- Creatures with lethal damage are destroyed
- Players with 0 or less life lose the game
- Planeswalkers with 0 loyalty counters are put into their owner's graveyard
#![allow(unused)] fn main() { pub fn check_combat_damage_results( creatures: Query<(Entity, &Creature, &Health)>, players: Query<(Entity, &Player, &Life)>, planeswalkers: Query<(Entity, &Planeswalker, &Loyalty)>, mut destroy_events: EventWriter<DestroyPermanentEvent>, mut player_loss_events: EventWriter<PlayerLossEvent>, ) { // Check for destroyed creatures for (entity, _, health) in creatures.iter() { if health.marked_damage >= health.toughness { destroy_events.send(DestroyPermanentEvent { entity: entity, reason: DestructionReason::LethalDamage, }); } } // Check for player losses and planeswalker destruction // ... } }