mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
- Fix tgogenerator - Fix UI for ForceGroup and Layouts - Fix ammo depot handling - Split bigger files in smaller meaningful files (TGO, layouts, forces) - Renamed Template to Layout - Renamed GroundGroup to TheaterGroup and GroundUnit to TheaterUnit - Reorganize Layouts and UnitGroups to a ArmedForces class and ForceGroup similar to the AirWing and Squadron - Reworded the UnitClass, GroupRole, GroupTask (adopted to PEP8) and reworked the connection from Role and Task - added comments - added missing unit classes - added temp workaround for missing classes - add repariable property to TheaterUnit - Review and Cleanup Added serialization for loaded templates Loading the templates from the .miz files takes a lot of computation time and in the future there will be more templates added to the system. Therefore a local pickle serialization for the loaded templates was re-added: - The pickle will be created the first time the TemplateLoader will be accessed - Pickle is stored in Liberation SaveDir - Added UI option to (re-)import templates
348 lines
16 KiB
Python
348 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_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_losses(debriefing: Debriefing) -> None:
|
|
for ground_object_loss in debriefing.ground_object_losses:
|
|
ground_object_loss.theater_unit.kill()
|
|
for scenery_object_loss in debriefing.scenery_object_losses:
|
|
scenery_object_loss.ground_unit.kill()
|
|
|
|
@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}",
|
|
)
|