mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
- completly refactored the way TGO handles groups and replaced the usage of the pydcs ground groups (vehicle, ship, static) with an own Group and Unit class. - this allows us to only take care of dcs group generation during miz generation, where it should have always been. - We can now have any type of unit (even statics) in the same logic ground group we handle in liberation. this is independent from the dcs group handling. the dcs group will only be genarted when takeoff is pressed. - Refactored the unitmap and the scenery object handling to adopt to changes that now TGOs can hold all Units we want. - Cleaned up many many many lines of uneeded hacks to build stuff around dcs groups. - Removed IDs for TGOs as the names we generate are unique and for liberation to work we need no ids. Unique IDs for dcs will be generated for the units and groups only.
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.ground_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}",
|
|
)
|