Multiplayer Combat
Overview
Commander is inherently a multiplayer format, which introduces unique complexities to the combat system. Unlike one-on-one matches, multiplayer combat allows a player to attack multiple opponents simultaneously and introduces political elements that affect combat decisions. This document details how the multiplayer aspects of combat are implemented, tested, and verified in our game engine.
Core Multiplayer Combat Features
Multiple Attack Targets
In Commander, the active player can declare attackers against different opponents in the same combat phase. This is implemented through the attack declaration system:
#![allow(unused)] fn main() { pub fn validate_multiplayer_attacks( combat_system: Res<CombatSystem>, turn_manager: Res<TurnManager>, player_query: Query<(Entity, &Player)>, mut game_events: EventWriter<GameEvent>, ) { let active_player = turn_manager.get_active_player(); // Group attacks by defender let mut attacks_by_defender: HashMap<Entity, Vec<Entity>> = HashMap::new(); for (attacker, attack_data) in &combat_system.attackers { attacks_by_defender.entry(attack_data.defender) .or_insert_with(Vec::new) .push(*attacker); } // Verify all defenders are opponents for (defender, attackers) in &attacks_by_defender { // Skip planeswalkers (they're handled separately) if !player_query.contains(*defender) { continue; } // Verify defender is not the active player if *defender == active_player { game_events.send(GameEvent::InvalidAttackTarget { attacker: attackers[0], // Just report one of the attackers defender: *defender, reason: "Cannot attack yourself".to_string(), }); } } // Log all attack declarations for game history game_events.send(GameEvent::MultiplayerAttacksDeclared { active_player, attacks_by_defender: attacks_by_defender.clone(), }); } }
Defending Player Choice
When a player is being attacked, they alone make blocking decisions for those attackers:
#![allow(unused)] fn main() { pub fn handle_multiplayer_blocks( mut combat_system: ResMut<CombatSystem>, turn_manager: Res<TurnManager>, mut block_events: EventReader<BlockDeclarationEvent>, player_query: Query<Entity, With<Player>>, ) { // Group attackers by defending player let mut attackers_by_defender: HashMap<Entity, Vec<Entity>> = HashMap::new(); for (attacker, attack_data) in &combat_system.attackers { if player_query.contains(attack_data.defender) { attackers_by_defender.entry(attack_data.defender) .or_insert_with(Vec::new) .push(*attacker); } } // Process block declarations from each defending player for event in block_events.iter() { let BlockDeclarationEvent { player, blocker, attacker } = event; // Verify the player is declaring blocks against attackers targeting them if let Some(attackers) = attackers_by_defender.get(player) { if attackers.contains(attacker) { // This is a valid block declaration if let Some(block_data) = combat_system.blockers.get_mut(blocker) { block_data.blocked_attackers.push(*attacker); } else { combat_system.blockers.insert(*blocker, BlockData { blocker: *blocker, blocked_attackers: vec![*attacker], requirements: Vec::new(), restrictions: Vec::new(), }); } } else { // Player trying to block an attacker not targeting them warn!("Player {:?} tried to block attacker {:?} not targeting them", player, attacker); } } } } }
Political Game Elements
Goad Mechanic
Goad is a Commander-focused mechanic that forces creatures to attack players other than you. This is implemented as follows:
#![allow(unused)] fn main() { #[derive(Component)] pub struct Goaded { pub source: Entity, pub until_end_of_turn: bool, } pub fn apply_goad_requirements( mut combat_system: ResMut<CombatSystem>, goaded_query: Query<(Entity, &Goaded, &Controllable)>, turn_manager: Res<TurnManager>, ) { let active_player = turn_manager.get_active_player(); // Find all goaded creatures controlled by the active player for (entity, goaded, controllable) in goaded_query.iter() { if controllable.controller == active_player { // Add attack requirement - must attack if able combat_system.add_attack_requirement(entity, AttackRequirement::MustAttack); // Add attack restriction - can't attack the goad source combat_system.add_attack_restriction(entity, AttackRestriction::CantAttackPlayer(goaded.source)); } } } pub fn cleanup_goad_effects( mut commands: Commands, goaded_query: Query<(Entity, &Goaded)>, turn_manager: Res<TurnManager>, ) { // Only run during end step if turn_manager.current_phase != Phase::Ending(EndingStep::End) { return; } // Remove goad effects that last until end of turn for (entity, goaded) in goaded_query.iter() { if goaded.until_end_of_turn { commands.entity(entity).remove::<Goaded>(); } } } }
Monarch Mechanic
The monarch is another multiplayer-focused mechanic that encourages combat:
#![allow(unused)] fn main() { #[derive(Resource)] pub struct Monarch(pub Option<Entity>); pub fn monarch_attack_trigger( mut monarch: ResMut<Monarch>, combat_system: Res<CombatSystem>, turn_manager: Res<TurnManager>, mut game_events: EventWriter<GameEvent>, ) { // Only check at combat damage step if turn_manager.current_phase != Phase::Combat(CombatStep::CombatDamage) { return; } // Find players who were dealt combat damage let mut damaged_players = HashSet::new(); for event in combat_system.combat_history.iter() { if let CombatEvent::DamageDealt { source, target, amount, .. } = event { if *amount > 0 && combat_system.attackers.contains_key(source) { damaged_players.insert(*target); } } } // Check if the monarch was dealt damage if let Some(current_monarch) = monarch.0 { if damaged_players.contains(¤t_monarch) { // Find who dealt damage to the monarch for event in combat_system.combat_history.iter() { if let CombatEvent::DamageDealt { source, target, amount, .. } = event { if *amount > 0 && *target == current_monarch && combat_system.attackers.contains_key(source) { // Get the controller of the attacking creature if let Some(controller) = get_controller(*source) { if controller != current_monarch { // Change the monarch monarch.0 = Some(controller); game_events.send(GameEvent::MonarchChanged { old_monarch: current_monarch, new_monarch: controller, reason: "Combat damage".to_string(), }); // Only change once (first damage) break; } } } } } } } } }
Multiplayer Damage Tracking
In multiplayer games, damage source tracking becomes more important, especially for commander damage:
#![allow(unused)] fn main() { pub fn track_multiplayer_combat_damage( combat_system: Res<CombatSystem>, mut player_query: Query<(Entity, &mut Player)>, commander_query: Query<Entity, With<Commander>>, mut game_events: EventWriter<GameEvent>, ) { // Extract all combat damage events let damage_events = combat_system.combat_history .iter() .filter_map(|event| { if let CombatEvent::DamageDealt { source, target, amount, is_commander_damage } = event { Some((*source, *target, *amount, *is_commander_damage)) } else { None } }) .collect::<Vec<_>>(); // Process commander damage for (source, target, amount, is_commander_damage) in damage_events { // Only process if the source is a commander and target is a player if is_commander_damage && commander_query.contains(source) { for (player_entity, mut player) in player_query.iter_mut() { if player_entity == target { // Update commander damage tracking let previous_damage = player.commander_damage.get(&source).copied().unwrap_or(0); let new_damage = previous_damage + amount; player.commander_damage.insert(source, new_damage); // Check for commander damage loss condition if new_damage >= 21 { game_events.send(GameEvent::PlayerLost { player: player_entity, reason: LossReason::CommanderDamage(source), }); } } } } } } }
Teamwork Mechanics
Some Commander variants include team play, which requires special handling:
#![allow(unused)] fn main() { #[derive(Component)] pub struct Team(pub u32); pub fn validate_team_attacks( combat_system: Res<CombatSystem>, team_query: Query<(Entity, &Team)>, mut game_events: EventWriter<GameEvent>, ) { // Create team mappings let mut player_teams = HashMap::new(); for (entity, team) in team_query.iter() { player_teams.insert(entity, team.0); } // Check for attacks against teammates for (attacker, attack_data) in &combat_system.attackers { if let Some(attacker_controller) = get_controller(*attacker) { if let (Some(attacker_team), Some(defender_team)) = ( player_teams.get(&attacker_controller), player_teams.get(&attack_data.defender) ) { if attacker_team == defender_team { // This is an attack against a teammate game_events.send(GameEvent::TeamAttack { attacker: *attacker, defender: attack_data.defender, team: *attacker_team, }); } } } } } }
Multiplayer Edge Cases
Player Elimination During Combat
If a player is eliminated during combat, any attackers targeting them need to be handled:
#![allow(unused)] fn main() { pub fn handle_player_elimination_during_combat( mut combat_system: ResMut<CombatSystem>, mut player_elimination_events: EventReader<PlayerEliminatedEvent>, mut commands: Commands, ) { for event in player_elimination_events.iter() { let eliminated_player = event.player; // Remove any attackers targeting the eliminated player let attackers_to_remove: Vec<Entity> = combat_system.attackers .iter() .filter_map(|(attacker, attack_data)| { if attack_data.defender == eliminated_player { Some(*attacker) } else { None } }) .collect(); for attacker in attackers_to_remove { combat_system.attackers.remove(&attacker); // Update creature component to no longer be attacking if let Some(mut creature) = commands.get_entity(attacker) { creature.insert(Creature { attacking: None, // Other fields preserved... ..Default::default() // This would be replaced with actual preservation }); } } // Remove any blockers controlled by the eliminated player let blockers_to_remove: Vec<Entity> = combat_system.blockers .iter() .filter_map(|(blocker, _)| { if get_controller(*blocker) == Some(eliminated_player) { Some(*blocker) } else { None } }) .collect(); for blocker in blockers_to_remove { combat_system.blockers.remove(&blocker); // Update creature component to no longer be blocking if let Some(mut creature) = commands.get_entity(blocker) { creature.insert(Creature { blocking: Vec::new(), // Other fields preserved... ..Default::default() // This would be replaced with actual preservation }); } } } } }
Redirect Attack Effects
Some cards can redirect attacks to different players:
#![allow(unused)] fn main() { pub fn handle_attack_redirection( mut combat_system: ResMut<CombatSystem>, redirection_effects: Query<(Entity, &AttackRedirection)>, ) { // Process any attack redirection effects let redirections: Vec<(Entity, Entity)> = combat_system.attackers .iter() .filter_map(|(attacker, attack_data)| { for (_, redirection) in redirection_effects.iter() { if redirection.original_defender == attack_data.defender && redirection.applies_to(*attacker) { return Some((*attacker, redirection.new_defender)); } } None }) .collect(); // Apply redirections for (attacker, new_defender) in redirections { if let Some(attack_data) = combat_system.attackers.get_mut(&attacker) { attack_data.defender = new_defender; } } } #[derive(Component)] pub struct AttackRedirection { pub original_defender: Entity, pub new_defender: Entity, pub condition: RedirectionCondition, } impl AttackRedirection { pub fn applies_to(&self, attacker: Entity) -> bool { match &self.condition { RedirectionCondition::AllAttackers => true, RedirectionCondition::AttackerWithPower { operator, value } => { // Implementation details omitted true }, // Other conditions... _ => false, } } } }
Testing Strategy
Multiplayer Combat Unit Tests
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn test_multiple_attack_targets() { let mut app = App::new(); app.add_systems(Update, validate_multiplayer_attacks); app.add_event::<GameEvent>(); // Set up test environment let active_player = app.world.spawn(Player::default()).id(); let opponent1 = app.world.spawn(Player::default()).id(); let opponent2 = app.world.spawn(Player::default()).id(); let attacker1 = app.world.spawn(Creature::default()).id(); let attacker2 = app.world.spawn(Creature::default()).id(); // Create turn manager let mut turn_manager = TurnManager::default(); turn_manager.active_player_index = 0; turn_manager.player_order = vec![active_player, opponent1, opponent2]; app.insert_resource(turn_manager); // Create combat system with attacks against multiple players let mut combat_system = CombatSystem::default(); combat_system.attackers.insert(attacker1, AttackData { attacker: attacker1, defender: opponent1, is_commander: false, requirements: Vec::new(), restrictions: Vec::new(), }); combat_system.attackers.insert(attacker2, AttackData { attacker: attacker2, defender: opponent2, is_commander: false, requirements: Vec::new(), restrictions: Vec::new(), }); app.insert_resource(combat_system); // Run the system app.update(); // Check for multiplay attack event let mut event_reader = app.world.resource_mut::<Events<GameEvent>>(); let mut found_event = false; for event in event_reader.iter() { if let GameEvent::MultiplayerAttacksDeclared { .. } = event { found_event = true; break; } } assert!(found_event, "Multiplayer attack event not emitted"); } #[test] fn test_goad_mechanic() { let mut app = App::new(); app.add_systems(Update, apply_goad_requirements); // Set up test environment let active_player = app.world.spawn(Player::default()).id(); let opponent1 = app.world.spawn(Player::default()).id(); let opponent2 = app.world.spawn(Player::default()).id(); // Create goaded creature let goaded_creature = app.world.spawn(( Creature::default(), Controllable { controller: active_player }, Goaded { source: opponent1, until_end_of_turn: true }, )).id(); // Create turn manager let mut turn_manager = TurnManager::default(); turn_manager.active_player_index = 0; turn_manager.player_order = vec![active_player, opponent1, opponent2]; app.insert_resource(turn_manager); // Create combat system let combat_system = CombatSystem::default(); app.insert_resource(combat_system); // Run the system app.update(); // Verify goad requirements were applied let combat_system = app.world.resource::<CombatSystem>(); // Check that creature must attack let has_must_attack = combat_system.attack_requirements.get(&goaded_creature) .map_or(false, |reqs| reqs.iter().any(|req| matches!(req, AttackRequirement::MustAttack))); assert!(has_must_attack, "Goaded creature should have MustAttack requirement"); // Check that creature can't attack goad source let has_cant_attack_source = combat_system.attack_restrictions.get(&goaded_creature) .map_or(false, |reqs| reqs.iter().any(|req| matches!(req, AttackRestriction::CantAttackPlayer(p) if *p == opponent1))); assert!(has_cant_attack_source, "Goaded creature should not be able to attack goad source"); } #[test] fn test_monarch_mechanic() { let mut app = App::new(); app.add_systems(Update, monarch_attack_trigger); app.add_event::<GameEvent>(); // Set up test environment let player1 = app.world.spawn(Player::default()).id(); let player2 = app.world.spawn(Player::default()).id(); let attacker = app.world.spawn(( Creature::default(), Controllable { controller: player2 }, )).id(); // Create turn manager with combat phase let mut turn_manager = TurnManager::default(); turn_manager.current_phase = Phase::Combat(CombatStep::CombatDamage); app.insert_resource(turn_manager); // Set player1 as monarch app.insert_resource(Monarch(Some(player1))); // Create combat system with attack history let mut combat_system = CombatSystem::default(); combat_system.attackers.insert(attacker, AttackData { attacker, defender: player1, is_commander: false, requirements: Vec::new(), restrictions: Vec::new(), }); combat_system.combat_history.push_back(CombatEvent::DamageDealt { source: attacker, target: player1, amount: 3, is_commander_damage: false, }); app.insert_resource(combat_system); // Run the system app.update(); // Verify monarch changed let monarch = app.world.resource::<Monarch>(); assert_eq!(monarch.0, Some(player2), "Monarch should have changed to player2"); } // Additional tests... } }
Multiplayer Combat Integration Tests
#![allow(unused)] fn main() { #[cfg(test)] mod integration_tests { use super::*; #[test] fn test_multiplayer_combat_sequence() { let mut builder = CombatScenarioBuilder::new(); // Add multiple opponents let opponent1 = builder.add_player(); let opponent2 = builder.add_player(); // Add attackers for active player let attacker1 = builder.add_attacker(3, 3, builder.active_player, false); let attacker2 = builder.add_attacker(2, 2, builder.active_player, true); // Commander // Add blocker for first opponent let blocker = builder.add_blocker(2, 2, opponent1); // Declare attacks against multiple opponents builder.declare_attacks(vec![ (attacker1, opponent1), (attacker2, opponent2), ]); // Declare blocks builder.declare_blocks(vec![ (blocker, vec![attacker1]), ]); // Execute combat let result = builder.execute(); // Verify results // Opponent1 should have lost no life (blocked) but blocker should be dead assert_eq!(result.player_life[&opponent1], 40); let blocker_status = result.creature_status.get(&blocker).unwrap(); assert!(blocker_status.destroyed); // Opponent2 should have taken commander damage assert_eq!(result.player_life[&opponent2], 40 - 2); assert_eq!(result.commander_damage[&opponent2][&attacker2], 2); } #[test] fn test_player_elimination_during_combat() { let mut app = App::new(); // Setup full game environment with multiple players // Implementation details omitted for brevity // Simulate combat damage that eliminates a player // Implementation details omitted for brevity // Verify that all attacks targeting the eliminated player are removed // Implementation details omitted for brevity } // Additional integration tests... } }
Multiplayer Combat System Tests
#![allow(unused)] fn main() { #[cfg(test)] mod system_tests { use super::*; #[test] fn test_full_multiplayer_game() { let mut app = App::new(); // Setup a 4-player Commander game let players = setup_four_player_game(&mut app); // Play through several turns with complex political interactions for _ in 0..10 { app.update(); // Add various political actions between updates // (goad effects, attack redirection, monarch changes, etc.) // Implementation details omitted for brevity } // Verify multiplayer combat interactions worked as expected // Implementation details omitted for brevity } // Helper functions for system tests fn setup_four_player_game(app: &mut App) -> Vec<Entity> { // Implementation details omitted for brevity Vec::new() } // Additional system tests... } }
Performance Considerations
Multiplayer combat introduces additional computational complexity:
#![allow(unused)] fn main() { // Optimize attack target lookup pub fn optimize_multiplayer_combat( mut combat_system: ResMut<CombatSystem>, ) { // Build attack target lookup table for faster validation let mut attack_target_map: HashMap<Entity, HashSet<Entity>> = HashMap::new(); for (attacker, attack_data) in &combat_system.attackers { attack_target_map.entry(attack_data.defender) .or_insert_with(HashSet::new) .insert(*attacker); } combat_system.attack_target_map = attack_target_map; } }
UI Considerations
Multiplayer combat requires special attention to the user interface:
#![allow(unused)] fn main() { pub fn update_multiplayer_combat_ui( combat_system: Res<CombatSystem>, player_query: Query<(Entity, &Player)>, mut ui_state: ResMut<UiState>, ) { // Group attacks by defender for UI display let mut attacks_by_defender: HashMap<Entity, Vec<Entity>> = HashMap::new(); for (attacker, attack_data) in &combat_system.attackers { attacks_by_defender.entry(attack_data.defender) .or_insert_with(Vec::new) .push(*attacker); } // Update UI state for each player for (player_entity, _) in player_query.iter() { // Display incoming attacks for this player if let Some(attackers) = attacks_by_defender.get(&player_entity) { ui_state.player_incoming_attacks.insert(player_entity, attackers.clone()); } else { ui_state.player_incoming_attacks.insert(player_entity, Vec::new()); } } } }
Networking Considerations
In a networked multiplayer game, commander-specific combat events need special handling:
#![allow(unused)] fn main() { pub fn replicate_multiplayer_combat_state( combat_system: Res<CombatSystem>, monarch: Res<Monarch>, mut replication: ResMut<Replication>, ) { // Replicate critical combat state replication.replicate_resource::<CombatSystem>(); replication.replicate_resource::<Monarch>(); // Replicate all combat-relevant components for (entity, _) in combat_system.attackers.iter() { replication.replicate_entity(*entity); } for (entity, _) in combat_system.blockers.iter() { replication.replicate_entity(*entity); } } }
Conclusion
Multiplayer combat in Commander adds significant complexity but also creates rich strategic gameplay. By properly implementing the systems described in this document, we ensure that players can fully experience the political dynamics and multiplayer interactions that make Commander such a beloved format. The implementation handles all the edge cases and unique mechanics while maintaining good performance even with four or more players.