End of Combat
Overview
The End of Combat step is the final phase of the combat sequence where "at end of combat" triggered abilities are put on the stack and resolved. This step also includes important cleanup activities that return the game state to normal after combat concludes. In Commander, this step is particularly important for handling multiplayer-specific combat interactions and resetting the combat state.
This document outlines the implementation details, edge cases, and testing strategies for the End of Combat step in our Commander engine.
Core Concepts
End of Combat Flow
The End of Combat step follows this general sequence:
- "At end of combat" triggered abilities are put on the stack
- Priority is passed to players in turn order
- Once all players pass priority and the stack is empty, combat cleanup occurs:
- "Until end of combat" effects end
- Combat-specific statuses are removed
- The game advances to the Postcombat Main Phase
Combat Cleanup
After the End of Combat step is complete, several important cleanup tasks must occur:
- Remove all attacking and blocking statuses from creatures
- End "until end of combat" effects
- Clear combat damage tracking from the current turn
- Reset any combat-specific flags or state
Implementation Design
Data Structures
#![allow(unused)] fn main() { // Component for tracking "until end of combat" effects #[derive(Component)] struct UntilEndOfCombat { // Any data needed for the effect } // System resource for managing the end of combat step struct EndOfCombatSystem { triggers_processed: bool, } }
End of Combat System
#![allow(unused)] fn main() { fn end_of_combat_system( mut commands: Commands, mut combat_system: ResMut<CombatSystem>, turn_manager: Res<TurnManager>, attacker_query: Query<Entity, With<Attacking>>, blocker_query: Query<Entity, With<Blocking>>, end_of_combat_effects: Query<Entity, With<UntilEndOfCombat>>, end_of_combat_triggers: Query<(Entity, &EndOfCombatTrigger)>, // Other system parameters ) { // Only run during end of combat step if !matches!(turn_manager.current_phase, Phase::Combat(CombatStep::End)) { return; } // Process "at end of combat" triggers if not already processed if !combat_system.end_of_combat.triggers_processed { for (entity, trigger) in end_of_combat_triggers.iter() { // Create trigger event commands.spawn(TriggerEvent { source: entity, trigger_type: TriggerType::EndOfCombat, }); } combat_system.end_of_combat.triggers_processed = true; return; // Exit to allow triggers to be processed } // If we've processed triggers and the stack is empty, perform combat cleanup if combat_system.end_of_combat.triggers_processed && turn_manager.stack_is_empty() { // Remove attacking status from all attackers for entity in attacker_query.iter() { commands.entity(entity).remove::<Attacking>(); } // Remove blocking status from all blockers for entity in blocker_query.iter() { commands.entity(entity).remove::<Blocking>(); } // End "until end of combat" effects for entity in end_of_combat_effects.iter() { commands.entity(entity).remove::<UntilEndOfCombat>(); } // Reset combat system state combat_system.reset(); // Combat complete, ready to advance to postcombat main phase } } }
Combat System Reset
#![allow(unused)] fn main() { impl CombatSystem { pub fn reset(&mut self) { self.attackers.clear(); self.blockers.clear(); self.damage_assignments.clear(); self.first_strike_round_completed = false; self.begin_combat.triggers_processed = false; self.declare_attackers.triggers_processed = false; self.declare_blockers.triggers_processed = false; self.end_of_combat.triggers_processed = false; } } }
Special Cases and Edge Scenarios
Delayed End of Combat Triggers
Some effects create delayed triggered abilities that trigger at the end of combat:
#![allow(unused)] fn main() { fn create_delayed_end_of_combat_trigger( commands: &mut Commands, source: Entity, effect: impl Fn(&mut Commands) + Send + Sync + 'static ) { commands.spawn(( DelayedTrigger { source, trigger_type: TriggerType::EndOfCombat, effect: Box::new(effect), }, )); } }
Regeneration Shield Cleanup
Regeneration shields that were used during combat should be removed:
#![allow(unused)] fn main() { fn cleanup_regeneration_shields( mut commands: Commands, regeneration_query: Query<(Entity, &RegenerationShield)> ) { for (entity, shield) in regeneration_query.iter() { if shield.used { commands.entity(entity).remove::<RegenerationShield>(); } } } }
"Until End of Combat" Effects
Effects that last until end of combat need special handling:
#![allow(unused)] fn main() { fn apply_until_end_of_combat_effect( commands: &mut Commands, target: Entity, effect_data: EffectData ) { // Apply the effect commands.entity(target).insert(effect_data.component); // Mark it to be removed at end of combat commands.entity(target).insert(UntilEndOfCombat { // Any necessary data }); } }
Phasing During End of Combat
Creatures that phase out during combat might need special handling during end of combat:
#![allow(unused)] fn main() { fn handle_phased_combat_participants( mut commands: Commands, phased_attackers: Query<Entity, (With<Attacking>, With<PhasedOut>)>, phased_blockers: Query<Entity, (With<Blocking>, With<PhasedOut>)> ) { // Remove combat status from phased creatures too for entity in phased_attackers.iter() { commands.entity(entity).remove::<Attacking>(); } for entity in phased_blockers.iter() { commands.entity(entity).remove::<Blocking>(); } } }
Testing Strategy
Unit Tests
#![allow(unused)] fn main() { #[test] fn test_basic_end_of_combat_cleanup() { // Set up test world let mut app = App::new(); app.add_plugins(MinimalPlugins) .add_systems(Update, end_of_combat_system); // Create attackers and blockers let attacker = app.world.spawn(( Creature { power: 3, toughness: 3 }, Attacking { defending: player_entity }, )).id(); let blocker = app.world.spawn(( Creature { power: 2, toughness: 2 }, Blocking { blocked_attackers: vec![attacker] }, )).id(); // Simulate end of combat step app.world.resource_mut::<TurnManager>().current_phase = Phase::Combat(CombatStep::End); app.world.resource_mut::<CombatSystem>().end_of_combat.triggers_processed = true; // Process end of combat app.update(); // Verify cleanup assert!(!app.world.entity(attacker).contains::<Attacking>()); assert!(!app.world.entity(blocker).contains::<Blocking>()); } #[test] fn test_end_of_combat_triggers() { // Test triggers that happen at end of combat // ... } #[test] fn test_until_end_of_combat_effects() { // Test that effects with "until end of combat" duration are removed // ... } }
Integration Tests
#![allow(unused)] fn main() { #[test] fn test_full_combat_sequence_with_end_phase() { // Test a complete combat sequence from beginning to end // ... } #[test] fn test_end_of_combat_with_replacement_effects() { // Test end of combat with replacement effects that modify cleanup // ... } }
Edge Case Tests
#![allow(unused)] fn main() { #[test] fn test_end_of_combat_with_phased_creatures() { // Test end of combat with phased creatures // ... } #[test] fn test_end_of_combat_when_player_loses() { // Test what happens if a player loses during end of combat // ... } #[test] fn test_end_of_combat_with_delayed_triggers() { // Test delayed triggers that happen at end of combat // ... } }
Performance Considerations
-
Batch Removal Operations: Group similar removal operations together for better performance.
-
Minimize Query Iterations: Structure queries to minimize iterations over entities.
-
State Reset Optimization: Efficiently reset the combat state without unnecessary operations.
-
Effect Tracking: Use an efficient system for tracking and removing "until end of combat" effects.
Conclusion
The End of Combat step is a critical transition point in the Commander game flow. Proper implementation ensures that combat-related statuses and effects are appropriately cleaned up, and the game state is correctly prepared for the next phase. By handling all "at end of combat" triggers and cleanup operations correctly, we create a robust and reliable combat system that maintains game state integrity while supporting all the complex interactions present in the Commander format.