from __future__ import annotations import logging from typing import TYPE_CHECKING from game.debriefing import Debriefing from game.ground_forces.combat_stance import CombatStance from game.theater import ControlPoint from .gameupdateevents import GameUpdateEvents from ..ato.airtaaskingorder import AirTaskingOrder if TYPE_CHECKING: from ..game import Game MINOR_DEFEAT_INFLUENCE = 0.1 DEFEAT_INFLUENCE = 0.3 STRONG_DEFEAT_INFLUENCE = 0.5 class MissionResultsProcessor: def __init__(self, game: Game) -> None: self.game = game def commit(self, debriefing: Debriefing, events: GameUpdateEvents) -> None: logging.info("Committing mission results") self.commit_air_losses(debriefing) self.commit_pilot_experience() self.commit_front_line_losses(debriefing) self.commit_convoy_losses(debriefing) self.commit_cargo_ship_losses(debriefing) self.commit_airlift_losses(debriefing) self.commit_ground_losses(debriefing, events) self.commit_damaged_runways(debriefing) self.commit_captures(debriefing, events) self.commit_front_line_battle_impact(debriefing, events) self.record_carcasses(debriefing) def commit_air_losses(self, debriefing: Debriefing) -> None: for loss in debriefing.air_losses.losses: if loss.pilot is not None and ( not loss.pilot.player or not self.game.settings.invulnerable_player_pilots ): loss.pilot.kill() squadron = loss.flight.squadron aircraft = loss.flight.unit_type available = squadron.owned_aircraft if available <= 0: logging.error( f"Found killed {aircraft} from {squadron} but that airbase has " "none available." ) continue logging.info(f"{aircraft} destroyed from {squadron}") squadron.owned_aircraft -= 1 @staticmethod def _commit_pilot_experience(ato: AirTaskingOrder) -> None: for package in ato.packages: for flight in package.flights: for idx, pilot in enumerate(flight.roster.iter_pilots()): if pilot is None: logging.error( f"Cannot award experience to pilot #{idx} of {flight} " "because no pilot is assigned" ) continue pilot.record.missions_flown += 1 def commit_pilot_experience(self) -> None: self._commit_pilot_experience(self.game.blue.ato) self._commit_pilot_experience(self.game.red.ato) @staticmethod def commit_front_line_losses(debriefing: Debriefing) -> None: for loss in debriefing.front_line_losses: unit_type = loss.unit_type control_point = loss.origin available = control_point.base.total_units_of_type(unit_type) if available <= 0: logging.error( f"Found killed {unit_type} from {control_point} but that " "airbase has none available." ) continue logging.info(f"{unit_type} destroyed from {control_point}") control_point.base.armor[unit_type] -= 1 @staticmethod def commit_convoy_losses(debriefing: Debriefing) -> None: for loss in debriefing.convoy_losses: unit_type = loss.unit_type convoy = loss.convoy available = loss.convoy.units.get(unit_type, 0) convoy_name = f"convoy from {convoy.origin} to {convoy.destination}" if available <= 0: logging.error( f"Found killed {unit_type} in {convoy_name} but that convoy has " "none available." ) continue logging.info(f"{unit_type} destroyed in {convoy_name}") convoy.kill_unit(unit_type) @staticmethod def commit_cargo_ship_losses(debriefing: Debriefing) -> None: for ship in debriefing.cargo_ship_losses: logging.info( f"All units destroyed in cargo ship from {ship.origin} to " f"{ship.destination}." ) ship.kill_all() @staticmethod def commit_airlift_losses(debriefing: Debriefing) -> None: for loss in debriefing.airlift_losses: transfer = loss.transfer airlift_name = f"airlift from {transfer.origin} to {transfer.destination}" for unit_type in loss.cargo: try: transfer.kill_unit(unit_type) logging.info(f"{unit_type} destroyed in {airlift_name}") except KeyError: logging.exception( f"Found killed {unit_type} in {airlift_name} but that airlift " "has none available." ) @staticmethod def commit_ground_losses(debriefing: Debriefing, events: GameUpdateEvents) -> None: for ground_object_loss in debriefing.ground_object_losses: ground_object_loss.theater_unit.kill(events) for scenery_object_loss in debriefing.scenery_object_losses: scenery_object_loss.ground_unit.kill(events) @staticmethod def commit_damaged_runways(debriefing: Debriefing) -> None: for damaged_runway in debriefing.damaged_runways: damaged_runway.damage_runway() def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None: for captured in debriefing.base_captures: try: if captured.captured_by_player.is_blue: self.game.message( f"{captured.control_point} captured!", f"We took control of {captured.control_point}.", ) else: self.game.message( f"{captured.control_point} lost!", f"The enemy took control of {captured.control_point}.", ) captured.control_point.capture( self.game, events, captured.captured_by_player ) except Exception: logging.exception(f"Could not process base capture {captured}") for captured in debriefing.base_captures: logging.info(f"Will run redeploy for {captured.control_point}") self.redeploy_units(captured.control_point) def record_carcasses(self, debriefing: Debriefing) -> None: for destroyed_unit in debriefing.state_data.destroyed_statics: self.game.add_destroyed_units(destroyed_unit) def commit_front_line_battle_impact( self, debriefing: Debriefing, events: GameUpdateEvents ) -> None: for cp in self.game.theater.player_points(): enemy_cps = [e for e in cp.connected_points if not e.captured] for enemy_cp in enemy_cps: front_line = cp.front_line_with(enemy_cp) front_line.update_position() events.update_front_line(front_line) print( "Compute frontline progression for : " + cp.name + " to " + enemy_cp.name ) delta = 0.0 player_won = True status_msg: str = "" ally_casualties = debriefing.casualty_count(cp) enemy_casualties = debriefing.casualty_count(enemy_cp) ally_units_alive = cp.base.total_armor enemy_units_alive = enemy_cp.base.total_armor print(f"Remaining allied units: {ally_units_alive}") print(f"Remaining enemy units: {enemy_units_alive}") print(f"Allied casualties {ally_casualties}") print(f"Enemy casualties {enemy_casualties}") ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties) player_aggresive = cp.stances[enemy_cp.id] in [ CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH, ] if ally_units_alive == 0: player_won = False delta = STRONG_DEFEAT_INFLUENCE status_msg = f"No allied units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces suffer a strong defeat." elif enemy_units_alive == 0: player_won = True delta = STRONG_DEFEAT_INFLUENCE status_msg = f"No enemy units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces win a strong victory." elif cp.stances[enemy_cp.id] == CombatStance.RETREAT: player_won = False delta = STRONG_DEFEAT_INFLUENCE status_msg = f"Allied forces are retreating along the {cp.name}-{enemy_cp.name} frontline, suffering a strong defeat." else: if enemy_casualties > ally_casualties: player_won = True if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH: delta = STRONG_DEFEAT_INFLUENCE status_msg = f"Allied forces break through the {cp.name}-{enemy_cp.name} frontline, winning a strong victory" else: if ratio > 3: delta = STRONG_DEFEAT_INFLUENCE status_msg = f"Enemy casualties massively outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a strong victory." elif ratio < 1.5: delta = MINOR_DEFEAT_INFLUENCE status_msg = f"Enemy casualties minorly outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a minor victory." else: delta = DEFEAT_INFLUENCE status_msg = f"Enemy casualties outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces claim a victory." elif ally_casualties > enemy_casualties: if ( ally_units_alive > 2 * enemy_units_alive and player_aggresive ): # Even with casualties if the enemy is overwhelmed, they are going to lose ground player_won = True delta = MINOR_DEFEAT_INFLUENCE status_msg = f"Despite suffering losses, allied forces still outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a minor victory." elif ( ally_units_alive > 3 * enemy_units_alive and player_aggresive ): player_won = True delta = STRONG_DEFEAT_INFLUENCE status_msg = f"Despite suffering losses, allied forces still heavily outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a major victory." else: # But if the enemy is not outnumbered, we lose player_won = False if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH: delta = STRONG_DEFEAT_INFLUENCE status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces have overextended themselves, suffering a major defeat." else: delta = DEFEAT_INFLUENCE status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces suffer a defeat." # No progress with defensive strategies if player_won and cp.stances[enemy_cp.id] in [ CombatStance.DEFENSIVE, CombatStance.AMBUSH, ]: print( f"Allied forces have adopted a defensive stance along the {cp.name}-{enemy_cp.name} " f"frontline, making only limited progress." ) delta = MINOR_DEFEAT_INFLUENCE # Handle the case where there are no casualties at all on either side but both sides still have units if delta == 0.0: print(status_msg) self.game.message( "Frontline Report", f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.", ) else: if player_won: print(status_msg) cp.base.affect_strength(delta) enemy_cp.base.affect_strength(-delta) self.game.message( "Frontline Report", f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}", ) else: print(status_msg) enemy_cp.base.affect_strength(delta) cp.base.affect_strength(-delta) self.game.message( "Frontline Report", f"Our ground forces from {cp.name} are losing ground against the enemy forces from " f"{enemy_cp.name}. {status_msg}", ) def redeploy_units(self, cp: ControlPoint) -> None: """ " Auto redeploy units to newly captured base """ enemy_connected_cps = [ ocp for ocp in cp.connected_points if cp.captured != ocp.captured ] # If the newly captured cp does not have enemy connected cp, # then it is not necessary to redeploy frontline units there. if len(enemy_connected_cps) == 0: return ally_connected_cps = [ ocp for ocp in cp.transitive_connected_friendly_destinations() if cp.captured == ocp.captured and ocp.base.total_armor ] settings = cp.coalition.game.settings factor = ( settings.frontline_reserves_factor if cp.captured else settings.frontline_reserves_factor_red ) # From each ally cp, send reinforcements for ally_cp in sorted( ally_connected_cps, key=lambda x: len( [cp for cp in x.connected_points if x.captured != cp.captured] ), ): self.redeploy_between(cp, ally_cp) if cp.base.total_armor > factor * cp.deployable_front_line_units: break def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None: total_units_redeployed = 0 moved_units = {} settings = source.coalition.game.settings reserves = max( 1, ( settings.reserves_procurement_target if source.captured else settings.reserves_procurement_target_red ), ) total_units = source.base.total_armor reserves_factor = (reserves - 1) / total_units # slight underestimation source_frontline_count = len( [cp for cp in source.connected_points if not source.is_friendly_to(cp)] ) move_factor = max(0.0, 1 / (source_frontline_count + 1) - reserves_factor) for frontline_unit, count in source.base.armor.items(): moved_count = int(count * move_factor) moved_units[frontline_unit] = moved_count total_units_redeployed += moved_count destination.base.commission_units(moved_units) source.base.commit_losses(moved_units) # Also transfer pending deliveries. for unit_type, count in source.ground_unit_orders.units.items(): move_count = int(count * move_factor) source.ground_unit_orders.sell({unit_type: move_count}) destination.ground_unit_orders.order({unit_type: move_count}) total_units_redeployed += move_count if total_units_redeployed > 0: self.game.message( "Units redeployed", f"{total_units_redeployed} units have been redeployed from " f"{source.name} to {destination.name}", )