mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Track damage (#3351)
This PR partially addresses #3313 by: - Tracking DCS hit events and storing the unit hit point updates in state.json - Adding logic to kill aircraft when the hit points is reduced to 1, as opposed to the DCS logic of hit points to 0. This behavior allows Liberation to track deaths to parked aircraft, which are uncontrolled and seem to have different damage logic in DCS. - Tracking damage to TheaterGroundObjects across turns and killing the unit when the unit's hitpoints reduces to 1 or lower. Intention is to build on this PR by also tracking front line objects and statics (buildings). However, larger refactoring is required and so splitting those into a separate PR.
This commit is contained in:
parent
1ee1113e48
commit
6550400604
@ -4,7 +4,10 @@ Saves from 10.x are not compatible with 11.0.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.9.3.51704 Open Beta.
|
||||
* **[Engine]** Support for DCS 2.9.3.51704.
|
||||
* **[Campaign]** Improved tracking of parked aircraft deaths. Parked aircraft are now considered dead once sufficient damage is done, meaning guns, rockets and AGMs are viable weapons for OCA/Aircraft missions. Previously Liberation relied on DCS death tracking which required parked aircraft to be hit with more powerful weapons e.g. 2000lb bombs as they were uncontrolled.
|
||||
* **[Campaign]** Track damage to theater ground objects across turns. Damage can accumulate across turns leading to death of the unit. This behavior only applies to SAMs, ships and other units that appear on the Liberation map. Frontline units and buildings are not tracked (yet).
|
||||
|
||||
|
||||
## Fixes
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
import itertools
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
@ -9,7 +9,9 @@ from typing import (
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
from uuid import UUID
|
||||
@ -21,8 +23,10 @@ from game.theater import Airfield, ControlPoint
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.ato.flight import Flight
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.sim.simulationresults import SimulationResults
|
||||
from game.transfers import CargoShip
|
||||
from game.theater import TheaterUnit
|
||||
from game.unitmap import (
|
||||
AirliftUnits,
|
||||
ConvoyUnit,
|
||||
@ -90,6 +94,103 @@ class BaseCaptureEvent:
|
||||
captured_by_player: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnitHitpointUpdate(ABC):
|
||||
unit: Any
|
||||
hit_points: int
|
||||
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls, data: dict[str, Any], unit_map: UnitMap
|
||||
) -> Optional[UnitHitpointUpdate]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_dead(self) -> bool:
|
||||
# Use hit_points > 1 to indicate unit is alive, rather than >=1 (DCS logic) to account for uncontrolled units which often have a
|
||||
# health floor of 1
|
||||
if self.hit_points > 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlyingUnitHitPointUpdate(UnitHitpointUpdate):
|
||||
unit: FlyingUnit
|
||||
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls, data: dict[str, Any], unit_map: UnitMap
|
||||
) -> Optional[FlyingUnitHitPointUpdate]:
|
||||
unit = unit_map.flight(data["name"])
|
||||
if unit is None:
|
||||
return None
|
||||
return cls(unit, int(float(data["hit_points"])))
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
if to_player:
|
||||
return self.unit.flight.departure.captured
|
||||
return not self.unit.flight.departure.captured
|
||||
|
||||
|
||||
@dataclass
|
||||
class TheaterUnitHitPointUpdate(UnitHitpointUpdate):
|
||||
unit: TheaterUnitMapping
|
||||
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls, data: dict[str, Any], unit_map: UnitMap
|
||||
) -> Optional[TheaterUnitHitPointUpdate]:
|
||||
unit = unit_map.theater_units(data["name"])
|
||||
if unit is None:
|
||||
return None
|
||||
|
||||
if unit.theater_unit.unit_type is None:
|
||||
logging.debug(
|
||||
f"Ground unit {data['name']} does not have a valid unit type."
|
||||
)
|
||||
return None
|
||||
|
||||
if unit.theater_unit.hit_points is None:
|
||||
logging.debug(f"Ground unit {data['name']} does not have hit_points set.")
|
||||
return None
|
||||
|
||||
sim_hit_points = int(
|
||||
float(data["hit_points"])
|
||||
) # Hit points out of the sim i.e. new unit hit points - damage in this turn
|
||||
previous_turn_hit_points = (
|
||||
unit.theater_unit.hit_points
|
||||
) # Hit points at the end of the previous turn
|
||||
full_health_hit_points = (
|
||||
unit.theater_unit.unit_type.hit_points
|
||||
) # Hit points of a new unit
|
||||
|
||||
# Hit points left after damage this turn is subtracted from hit points at the end of the previous turn
|
||||
new_hit_points = previous_turn_hit_points - (
|
||||
full_health_hit_points - sim_hit_points
|
||||
)
|
||||
|
||||
return cls(unit, new_hit_points)
|
||||
|
||||
def is_dead(self) -> bool:
|
||||
# Some TheaterUnits can start with low health of around 1, make sure we don't always kill them off.
|
||||
if (
|
||||
self.unit.theater_unit.unit_type is not None
|
||||
and self.unit.theater_unit.unit_type.hit_points is not None
|
||||
and self.unit.theater_unit.unit_type.hit_points <= 1
|
||||
):
|
||||
return False
|
||||
return super().is_dead()
|
||||
|
||||
def is_friendly(self, to_player: bool) -> bool:
|
||||
return self.unit.theater_unit.ground_object.is_friendly(to_player)
|
||||
|
||||
def commit(self) -> None:
|
||||
self.unit.theater_unit.hit_points = self.hit_points
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StateData:
|
||||
#: True if the mission ended. If False, the mission exited abnormally.
|
||||
@ -108,6 +209,10 @@ class StateData:
|
||||
#: Mangled names of bases that were captured during the mission.
|
||||
base_capture_events: List[str]
|
||||
|
||||
# List of descriptions of damage done to units. Each list element is a dict like the following
|
||||
# {"name": "<damaged unit name>", "hit_points": <hit points as float>}
|
||||
unit_hit_point_updates: List[dict[str, Any]]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: Dict[str, Any], unit_map: UnitMap) -> StateData:
|
||||
def clean_unit_list(unit_list: List[Any]) -> List[str]:
|
||||
@ -147,6 +252,7 @@ class StateData:
|
||||
killed_ground_units=killed_ground_units,
|
||||
destroyed_statics=data["destroyed_objects_positions"],
|
||||
base_capture_events=data["base_capture_events"],
|
||||
unit_hit_point_updates=data["unit_hit_point_updates"],
|
||||
)
|
||||
|
||||
|
||||
@ -284,6 +390,19 @@ class Debriefing:
|
||||
player_losses.append(aircraft)
|
||||
else:
|
||||
enemy_losses.append(aircraft)
|
||||
|
||||
for unit_data in self.state_data.unit_hit_point_updates:
|
||||
damaged_unit = FlyingUnitHitPointUpdate.from_json(unit_data, self.unit_map)
|
||||
if damaged_unit is None:
|
||||
continue
|
||||
if damaged_unit.is_dead():
|
||||
# If unit already killed, nothing to do.
|
||||
if unit_data["name"] in self.state_data.killed_aircraft:
|
||||
continue
|
||||
if damaged_unit.is_friendly(to_player=True):
|
||||
player_losses.append(damaged_unit.unit)
|
||||
else:
|
||||
enemy_losses.append(damaged_unit.unit)
|
||||
return AirLosses(player_losses, enemy_losses)
|
||||
|
||||
def dead_ground_units(self) -> GroundLosses:
|
||||
@ -356,8 +475,29 @@ class Debriefing:
|
||||
losses.enemy_airlifts.append(airlift_unit)
|
||||
continue
|
||||
|
||||
for unit_data in self.state_data.unit_hit_point_updates:
|
||||
damaged_unit = TheaterUnitHitPointUpdate.from_json(unit_data, self.unit_map)
|
||||
if damaged_unit is None:
|
||||
continue
|
||||
if damaged_unit.is_dead():
|
||||
if unit_data["name"] in self.state_data.killed_ground_units:
|
||||
continue
|
||||
if damaged_unit.is_friendly(to_player=True):
|
||||
losses.player_ground_objects.append(damaged_unit.unit)
|
||||
else:
|
||||
losses.enemy_ground_objects.append(damaged_unit.unit)
|
||||
|
||||
return losses
|
||||
|
||||
def unit_hit_point_update_events(self) -> List[TheaterUnitHitPointUpdate]:
|
||||
damaged_units = []
|
||||
for unit_data in self.state_data.unit_hit_point_updates:
|
||||
unit = TheaterUnitHitPointUpdate.from_json(unit_data, self.unit_map)
|
||||
if unit is None:
|
||||
continue
|
||||
damaged_units.append(unit)
|
||||
return damaged_units
|
||||
|
||||
def base_capture_events(self) -> List[BaseCaptureEvent]:
|
||||
"""Keeps only the last instance of a base capture event for each base ID."""
|
||||
blue_coalition_id = 2
|
||||
|
||||
@ -34,6 +34,7 @@ class MissionResultsProcessor:
|
||||
self.commit_damaged_runways(debriefing)
|
||||
self.commit_captures(debriefing, events)
|
||||
self.commit_front_line_battle_impact(debriefing, events)
|
||||
self.commit_unit_damage(debriefing)
|
||||
self.record_carcasses(debriefing)
|
||||
|
||||
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||
@ -307,6 +308,14 @@ class MissionResultsProcessor:
|
||||
f"{enemy_cp.name}. {status_msg}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def commit_unit_damage(debriefing: Debriefing) -> None:
|
||||
for damaged_unit in debriefing.unit_hit_point_update_events():
|
||||
logging.info(
|
||||
f"{damaged_unit.unit.theater_unit.name} damaged, setting hit points to {damaged_unit.hit_points}"
|
||||
)
|
||||
damaged_unit.commit()
|
||||
|
||||
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||
""" "
|
||||
Auto redeploy units to newly captured base
|
||||
|
||||
@ -35,6 +35,8 @@ class TheaterUnit:
|
||||
position: PointWithHeading
|
||||
# The parent ground object
|
||||
ground_object: TheaterGroundObject
|
||||
# Number of hit points the unit has
|
||||
hit_points: Optional[int] = None
|
||||
# State of the unit, dead or alive
|
||||
alive: bool = True
|
||||
|
||||
@ -42,13 +44,17 @@ class TheaterUnit:
|
||||
def from_template(
|
||||
id: int, dcs_type: Type[DcsUnitType], t: LayoutUnit, go: TheaterGroundObject
|
||||
) -> TheaterUnit:
|
||||
return TheaterUnit(
|
||||
unit = TheaterUnit(
|
||||
id,
|
||||
t.name,
|
||||
dcs_type,
|
||||
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
||||
go,
|
||||
)
|
||||
# if the TheaterUnit represents a GroundUnitType or ShipUnitType, initialize health to full hit points
|
||||
if unit.unit_type is not None:
|
||||
unit.hit_points = unit.unit_type.hit_points
|
||||
return unit
|
||||
|
||||
@property
|
||||
def unit_type(self) -> Optional[UnitType[Any]]:
|
||||
@ -70,14 +76,12 @@ class TheaterUnit:
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
dead_label = " [DEAD]" if not self.alive else ""
|
||||
unit_label = self.unit_type or self.type.name or self.name
|
||||
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
|
||||
return f"{str(self.id).zfill(4)} | {unit_label}{self._status_label()}"
|
||||
|
||||
@property
|
||||
def short_name(self) -> str:
|
||||
dead_label = " [DEAD]" if not self.alive else ""
|
||||
return f"<b>{self.type.id[0:18]}</b> {dead_label}"
|
||||
return f"<b>{self.type.id[0:18]}</b> {self._status_label()}"
|
||||
|
||||
@property
|
||||
def is_static(self) -> bool:
|
||||
@ -117,6 +121,18 @@ class TheaterUnit:
|
||||
unit_range = getattr(self.type, "threat_range", None)
|
||||
return meters(unit_range if unit_range is not None and self.alive else 0)
|
||||
|
||||
def _status_label(self) -> str:
|
||||
if not self.alive:
|
||||
return " [DEAD]"
|
||||
if self.unit_type is None:
|
||||
return ""
|
||||
if self.hit_points is None:
|
||||
return ""
|
||||
if self.unit_type.hit_points == self.hit_points:
|
||||
return ""
|
||||
damage_percentage = 100 - int(100 * self.hit_points / self.unit_type.hit_points)
|
||||
return f" [DAMAGED {damage_percentage}%]"
|
||||
|
||||
|
||||
class SceneryUnit(TheaterUnit):
|
||||
"""Special TheaterUnit for handling scenery ground objects"""
|
||||
|
||||
@ -11,6 +11,7 @@ kill_events = {} -- killed units will be added via S_EVENT_KILL
|
||||
base_capture_events = {}
|
||||
destroyed_objects_positions = {} -- will be added via S_EVENT_DEAD event
|
||||
killed_ground_units = {} -- keep track of static ground object deaths
|
||||
unit_hit_point_updates = {} -- stores updates to unit hit points, triggered by S_EVENT_HIT
|
||||
mission_ended = false
|
||||
|
||||
local function ends_with(str, ending)
|
||||
@ -41,6 +42,7 @@ function write_state()
|
||||
["mission_ended"] = mission_ended,
|
||||
["destroyed_objects_positions"] = destroyed_objects_positions,
|
||||
["killed_ground_units"] = killed_ground_units,
|
||||
["unit_hit_point_updates"] = unit_hit_point_updates,
|
||||
}
|
||||
if not json then
|
||||
local message = string.format("Unable to save DCS Liberation state to %s, JSON library is not loaded !", _debriefing_file_location)
|
||||
@ -146,6 +148,14 @@ write_state_error_handling = function()
|
||||
mist.scheduleFunction(write_state_error_handling, {}, timer.getTime() + WRITESTATE_SCHEDULE_IN_SECONDS)
|
||||
end
|
||||
|
||||
function update_hit_points(event)
|
||||
local update = {}
|
||||
update.name = event.target:getName()
|
||||
update.hit_points = event.target:getLife()
|
||||
unit_hit_point_updates[#unit_hit_point_updates + 1] = update
|
||||
write_state()
|
||||
end
|
||||
|
||||
activeWeapons = {}
|
||||
local function onEvent(event)
|
||||
if event.id == world.event.S_EVENT_CRASH and event.initiator then
|
||||
@ -175,6 +185,15 @@ local function onEvent(event)
|
||||
destroyed_objects_positions[#destroyed_objects_positions + 1] = destruction
|
||||
write_state()
|
||||
end
|
||||
|
||||
if event.id == world.event.S_EVENT_HIT then
|
||||
target_category = event.target:getCategory()
|
||||
if target_category == Object.Category.UNIT then
|
||||
-- check on the health of the target 1 second after as the life value is sometimes not updated
|
||||
-- at the time of the event
|
||||
timer.scheduleFunction(update_hit_points, event, timer.getTime() + 1)
|
||||
end
|
||||
end
|
||||
|
||||
if event.id == world.event.S_EVENT_MISSION_END then
|
||||
mission_ended = true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user