dcs_liberation/game/sim/missionresultsprocessor.py
2021-10-22 14:36:51 -07:00

361 lines
16 KiB
Python

from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from game.debriefing import Debriefing
from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance
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) -> 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_object_losses(debriefing)
self.commit_building_losses(debriefing)
self.commit_damaged_runways(debriefing)
self.commit_captures(debriefing)
self.commit_front_line_battle_impact(debriefing)
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.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_object_losses(debriefing: Debriefing) -> None:
for loss in debriefing.ground_object_losses:
# TODO: This should be stored in the TGO, not in the pydcs Group.
if not hasattr(loss.group, "units_losts"):
loss.group.units_losts = [] # type: ignore
loss.group.units.remove(loss.unit)
loss.group.units_losts.append(loss.unit) # type: ignore
def commit_building_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.building_losses:
loss.ground_object.kill()
self.game.message(
"Building destroyed",
f"{loss.ground_object.dcs_identifier} has been destroyed at "
f"location {loss.ground_object.obj_name}",
)
@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) -> None:
for captured in debriefing.base_captures:
try:
if captured.captured_by_player:
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, captured.captured_by_player)
logging.info(f"Will run redeploy for {captured.control_point}")
self.redeploy_units(captured.control_point)
except Exception:
logging.exception(f"Could not process base capture {captured}")
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) -> 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:
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
"""
ally_connected_cps = [
ocp for ocp in cp.connected_points if cp.captured == ocp.captured
]
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
# From each ally cp, send reinforcements
for ally_cp in ally_connected_cps:
self.redeploy_between(cp, ally_cp)
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
total_units_redeployed = 0
moved_units = {}
if source.has_active_frontline or not destination.captured:
# If there are still active front lines to defend at the
# transferring CP we should not transfer all units.
#
# Opfor also does not transfer all of their units.
# TODO: Balance the CPs rather than moving half from everywhere.
move_factor = 0.5
else:
# Otherwise we can move everything.
move_factor = 1
for frontline_unit, count in source.base.armor.items():
moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int(count * move_factor)
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}",
)