Refactor ground objects and prepare template system

- 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.
This commit is contained in:
RndName 2022-01-18 09:56:04 +01:00
parent 6baf36c587
commit d154069877
28 changed files with 603 additions and 745 deletions

View File

@ -10,7 +10,7 @@ from game.ato.flighttype import FlightType
@dataclass
class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]):
class PlanStrike(PackagePlanningTask[TheaterGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool:
if self.target not in state.strike_targets:
return False

View File

@ -57,7 +57,7 @@ class TheaterState(WorldState["TheaterState"]):
enemy_ships: list[NavalGroundObject]
enemy_garrisons: dict[ControlPoint, Garrisons]
oca_targets: list[ControlPoint]
strike_targets: list[TheaterGroundObject[Any]]
strike_targets: list[TheaterGroundObject]
enemy_barcaps: list[ControlPoint]
threat_zones: ThreatZones

View File

@ -1,4 +1,3 @@
from dcs.unit import Unit
from dcs.vehicles import AirDefence
@ -38,5 +37,5 @@ class AlicCodes:
}
@classmethod
def code_for(cls, unit: Unit) -> int:
return cls.CODES[unit.type]
def code_for(cls, unit_type: str) -> int:
return cls.CODES[unit_type]

View File

@ -5,6 +5,7 @@ from dcs.planes import plane_map
from dcs.ships import ship_map
from dcs.unittype import UnitType
from dcs.vehicles import vehicle_map
from dcs.statics import fortification_map, groundobject_map, warehouse_map, cargo_map
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
@ -16,5 +17,19 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
return ship_map[name]
if name in helicopter_map:
return helicopter_map[name]
else:
# Try statics
return static_type_from_name(name)
def static_type_from_name(name: str) -> Optional[Type[UnitType]]:
if name in fortification_map:
return fortification_map[name]
elif name in groundobject_map:
return groundobject_map[name]
elif name in warehouse_map:
return warehouse_map[name]
if name in cargo_map:
return cargo_map[name]
else:
return None

View File

@ -23,12 +23,12 @@ if TYPE_CHECKING:
from game.transfers import CargoShip
from game.unitmap import (
AirliftUnits,
Building,
ConvoyUnit,
FlyingUnit,
FrontLineUnit,
GroundObjectUnit,
GroundObjectMapping,
UnitMap,
SceneryObjectMapping,
)
DEBRIEFING_LOG_EXTENSION = "log"
@ -72,11 +72,11 @@ class GroundLosses:
player_airlifts: List[AirliftUnits] = field(default_factory=list)
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
player_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
player_ground_objects: List[GroundObjectMapping] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectMapping] = field(default_factory=list)
player_buildings: List[Building] = field(default_factory=list)
enemy_buildings: List[Building] = field(default_factory=list)
player_scenery: List[SceneryObjectMapping] = field(default_factory=list)
enemy_scenery: List[SceneryObjectMapping] = field(default_factory=list)
player_airfields: List[Airfield] = field(default_factory=list)
enemy_airfields: List[Airfield] = field(default_factory=list)
@ -158,14 +158,14 @@ class Debriefing:
yield from self.ground_losses.enemy_airlifts
@property
def ground_object_losses(self) -> Iterator[GroundObjectUnit[Any]]:
def ground_object_losses(self) -> Iterator[GroundObjectMapping]:
yield from self.ground_losses.player_ground_objects
yield from self.ground_losses.enemy_ground_objects
@property
def building_losses(self) -> Iterator[Building]:
yield from self.ground_losses.player_buildings
yield from self.ground_losses.enemy_buildings
def scenery_object_losses(self) -> Iterator[SceneryObjectMapping]:
yield from self.ground_losses.player_scenery
yield from self.ground_losses.enemy_scenery
@property
def damaged_runways(self) -> Iterator[Airfield]:
@ -217,17 +217,32 @@ class Debriefing:
losses_by_type[unit_type] += 1
return losses_by_type
def building_losses_by_type(self, player: bool) -> Dict[str, int]:
def ground_object_losses_by_type(self, player: bool) -> Dict[str, int]:
losses_by_type: Dict[str, int] = defaultdict(int)
if player:
losses = self.ground_losses.player_buildings
losses = self.ground_losses.player_ground_objects
else:
losses = self.ground_losses.enemy_buildings
losses = self.ground_losses.enemy_ground_objects
for loss in losses:
if loss.ground_object.control_point.captured != player:
continue
# We do not have handling for ships and statics UniType yet so we have to
# take more care here. Fallback for ship and static is to use the type str
# which is the dcs_type.id
unit_type = (
loss.ground_unit.unit_type.name
if loss.ground_unit.unit_type
else loss.ground_unit.type
)
losses_by_type[unit_type] += 1
return losses_by_type
losses_by_type[loss.ground_object.dcs_identifier] += 1
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]:
losses_by_type: Dict[str, int] = defaultdict(int)
if player:
losses = self.ground_losses.player_scenery
else:
losses = self.ground_losses.enemy_scenery
for loss in losses:
losses_by_type[loss.trigger_zone.name] += 1
return losses_by_type
def dead_aircraft(self) -> AirLosses:
@ -271,25 +286,21 @@ class Debriefing:
losses.enemy_cargo_ships.append(cargo_ship)
continue
ground_object_unit = self.unit_map.ground_object_unit(unit_name)
if ground_object_unit is not None:
if ground_object_unit.ground_object.control_point.captured:
losses.player_ground_objects.append(ground_object_unit)
ground_object = self.unit_map.ground_object(unit_name)
if ground_object is not None:
if ground_object.ground_unit.ground_object.is_friendly(to_player=True):
losses.player_ground_objects.append(ground_object)
else:
losses.enemy_ground_objects.append(ground_object_unit)
losses.enemy_ground_objects.append(ground_object)
continue
building = self.unit_map.building_or_fortification(unit_name)
scenery_object = self.unit_map.scenery_object(unit_name)
# Try appending object to the name, because we do this for building statics.
if building is None:
building = self.unit_map.building_or_fortification(
f"{unit_name} object"
)
if building is not None:
if building.ground_object.control_point.captured:
losses.player_buildings.append(building)
if scenery_object is not None:
if scenery_object.ground_unit.ground_object.is_friendly(to_player=True):
losses.player_scenery.append(scenery_object)
else:
losses.enemy_buildings.append(building)
losses.enemy_scenery.append(scenery_object)
continue
airfield = self.unit_map.airfield(unit_name)

View File

@ -45,6 +45,7 @@ if TYPE_CHECKING:
from .navmesh import NavMesh
from .squadrons import AirWing
from .threatzones import ThreatZones
from .factions.faction import Faction
COMMISION_UNIT_VARIETY = 4
COMMISION_LIMITS_SCALE = 1.5

View File

@ -19,7 +19,9 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
elif isinstance(target, MultiGroupTransport):
group_names.append(target.name)
elif isinstance(target, NavalControlPoint):
group_names.append(target.get_carrier_group_name())
carrier_name = target.get_carrier_group_name()
if carrier_name:
group_names.append(carrier_name)
else:
logging.error(
"Unexpected target type for BAI mission: %s",
@ -28,12 +30,12 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
return
for group_name in group_names:
group = self.mission.find_group(group_name)
if group is None:
miz_group = self.mission.find_group(group_name)
if miz_group is None:
logging.error("Could not find group for BAI mission %s", group_name)
continue
task = AttackGroup(group.id, weapon_type=WeaponType.Auto)
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto)
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False

View File

@ -12,7 +12,7 @@ from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.airsupport import AirSupport
from game.theater import MissionTarget
from game.theater import MissionTarget, GroundUnit
TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC,
@ -82,7 +82,7 @@ class PydcsWaypointBuilder:
return False
def register_special_waypoints(
self, targets: Iterable[Union[MissionTarget, Unit]]
self, targets: Iterable[Union[MissionTarget, GroundUnit]]
) -> None:
"""Create special target waypoints for various aircraft"""
for i, t in enumerate(targets):

View File

@ -32,7 +32,6 @@ from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple
from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission
from dcs.unit import Unit
from tabulate import tabulate
from game.ato.flighttype import FlightType
@ -41,7 +40,7 @@ from game.ato.flightwaypointtype import FlightWaypointType
from game.data.alic import AlicCodes
from game.dcs.aircrafttype import AircraftType
from game.radio.radios import RadioFrequency
from game.theater import ConflictTheater, LatLon, TheaterGroundObject
from game.theater import ConflictTheater, LatLon, TheaterGroundObject, GroundUnit
from game.theater.bullseye import Bullseye
from game.utils import Distance, UnitSystem, meters, mps, pounds
from game.weather import Weather
@ -608,14 +607,14 @@ class SeadTaskPage(KneeboardPage):
self.theater = theater
@property
def target_units(self) -> Iterator[Unit]:
def target_units(self) -> Iterator[GroundUnit]:
if isinstance(self.flight.package.target, TheaterGroundObject):
yield from self.flight.package.target.units
yield from self.flight.package.target.strike_targets
@staticmethod
def alic_for(unit: Unit) -> str:
def alic_for(unit_type: str) -> str:
try:
return str(AlicCodes.code_for(unit))
return str(AlicCodes.code_for(unit_type))
except KeyError:
return ""
@ -635,11 +634,15 @@ class SeadTaskPage(KneeboardPage):
writer.write(path)
def target_info_row(self, unit: Unit) -> List[str]:
def target_info_row(self, unit: GroundUnit) -> List[str]:
ll = self.theater.point_to_ll(unit.position)
unit_type = unit_type_from_name(unit.type)
name = unit.name if unit_type is None else unit_type.name
return [name, self.alic_for(unit), ll.format_dms(include_decimal_seconds=True)]
return [
name,
self.alic_for(unit.type),
ll.format_dms(include_decimal_seconds=True),
]
class StrikeTaskPage(KneeboardPage):

View File

@ -20,6 +20,8 @@ from typing import (
TYPE_CHECKING,
Type,
TypeVar,
List,
Any,
Union,
)
@ -49,24 +51,23 @@ from dcs.task import (
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone
from dcs.unit import InvisibleFARP, Ship, Unit, Vehicle
from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup
from dcs.unitgroup import ShipGroup, StaticGroup, VehicleGroup, MovingGroup
from dcs.unittype import ShipType, StaticType, VehicleType
from dcs.vehicles import vehicle_map
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.dcs.helpers import unit_type_from_name
from game.dcs.helpers import static_type_from_name, unit_type_from_name
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
BuildingGroundObject,
CarrierGroundObject,
FactoryGroundObject,
GenericCarrierGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
SceneryGroundObject,
ShipGroundObject,
GroundGroup,
GroundUnit,
SceneryGroundUnit,
)
from game.unitmap import UnitMap
from game.utils import Heading, feet, knots, mps
@ -79,18 +80,12 @@ FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any])
class GenericGroundObjectGenerator(Generic[TgoT]):
"""An unspecialized ground object generator.
Currently used only for SAM
"""
class GroundObjectGenerator:
"""generates the DCS groups and units from the TheaterGroundObject"""
def __init__(
self,
ground_object: TgoT,
ground_object: TheaterGroundObject,
country: Country,
game: Game,
mission: Mission,
@ -106,7 +101,7 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
def culled(self) -> bool:
return self.game.iads_considerate_culling(self.ground_object)
def generate(self) -> None:
def generate(self, unique_name: bool = True) -> None:
if self.culled:
return
@ -114,29 +109,102 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
if not group.units:
logging.warning(f"Found empty group in {self.ground_object}")
continue
group_name = group.group_name if unique_name else group.name
if group.static_group:
# Static Group
for i, u in enumerate(group.units):
if isinstance(u, SceneryGroundUnit):
# Special handling for scenery objects:
# Only create a trigger zone and no "real" dcs unit
self.add_trigger_zone_for_scenery(u)
continue
unit_type = vehicle_map[group.units[0].type]
vg = self.m.vehicle_group(
self.country,
group.name,
unit_type,
position=group.position,
heading=group.units[0].heading,
)
vg.units[0].name = group.units[0].name
vg.units[0].player_can_drive = True
for i, u in enumerate(group.units):
if i > 0:
vehicle = Vehicle(self.m.next_unit_id(), u.name, u.type)
vehicle.position.x = u.position.x
vehicle.position.y = u.position.y
vehicle.heading = u.heading
vehicle.player_can_drive = True
vg.add_unit(vehicle)
# Only skip dead units after trigger zone for scenery created!
if not u.alive:
continue
self.enable_eplrs(vg, unit_type)
self.set_alarm_state(vg)
self._register_unit_group(group, vg)
unit_type = static_type_from_name(u.type)
if not unit_type:
raise RuntimeError(
f"Unit type {u.type} is not a valid dcs unit type"
)
sg = self.m.static_group(
country=self.country,
name=u.unit_name if unique_name else u.name,
_type=unit_type,
position=u.position,
heading=u.position.heading.degrees,
dead=not u.alive,
)
self._register_ground_unit(u, sg.units[0])
else:
# Moving Group
moving_group: Optional[MovingGroup[Any]] = None
for i, unit in enumerate(group.units):
if not unit.alive:
continue
if unit.type in vehicle_map:
# Vehicle Group
unit_type = vehicle_map[unit.type]
elif unit.type in ship_map:
# Ship Group
unit_type = ship_map[group.units[0].type]
else:
raise RuntimeError(
f"Unit type {unit.type} is not a valid dcs unit type"
)
unit_name = unit.unit_name if unique_name else unit.name
if moving_group is None:
# First unit of the group will create the dcs group
if issubclass(unit_type, VehicleType):
moving_group = self.m.vehicle_group(
self.country,
group_name,
unit_type,
position=unit.position,
heading=unit.position.heading.degrees,
)
moving_group.units[0].player_can_drive = True
self.enable_eplrs(moving_group, unit_type)
if issubclass(unit_type, ShipType):
moving_group = self.m.ship_group(
self.country,
group_name,
unit_type,
position=unit.position,
heading=unit.position.heading.degrees,
)
if moving_group:
moving_group.units[0].name = unit_name
self.set_alarm_state(moving_group)
self._register_ground_unit(unit, moving_group.units[0])
else:
raise RuntimeError("DCS Group creation failed")
else:
# Additional Units in the group
dcs_unit: Optional[Unit] = None
if issubclass(unit_type, VehicleType):
dcs_unit = Vehicle(
self.m.next_unit_id(),
unit_name,
unit.type,
)
dcs_unit.player_can_drive = True
elif issubclass(unit_type, ShipType):
dcs_unit = Ship(
self.m.next_unit_id(),
unit_name,
unit_type,
)
if dcs_unit:
dcs_unit.position = unit.position
dcs_unit.heading = unit.position.heading.degrees
moving_group.add_unit(dcs_unit)
self._register_ground_unit(unit, dcs_unit)
else:
raise RuntimeError("DCS Unit creation failed")
@staticmethod
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
@ -149,24 +217,71 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
else:
group.points[0].tasks.append(OptAlarmState(1))
def _register_unit_group(
def _register_ground_unit(
self,
persistence_group: Union[ShipGroup, VehicleGroup],
miz_group: Union[ShipGroup, VehicleGroup],
ground_unit: GroundUnit,
dcs_unit: Unit,
) -> None:
self.unit_map.add_ground_object_units(
self.ground_object, persistence_group, miz_group
self.unit_map.add_ground_object_mapping(ground_unit, dcs_unit)
def add_trigger_zone_for_scenery(self, scenery: SceneryGroundUnit) -> None:
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
color = (
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
if scenery.ground_object.is_friendly(to_player=True)
else {1: 1, 2: 0.2, 3: 0.2, 4: 0.15}
)
# Create the smallest valid size trigger zone (16 feet) so that risk of overlap
# is minimized. As long as the triggerzone is over the scenery object, we're ok.
smallest_valid_radius = feet(16).meters
class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]):
trigger_zone = self.m.triggers.add_triggerzone(
scenery.zone.position,
smallest_valid_radius,
scenery.zone.hidden,
scenery.zone.name,
color,
scenery.zone.properties,
)
# DCS only visually shows a scenery object is dead when
# this trigger rule is applied. Otherwise you can kill a
# structure twice.
if not scenery.alive:
self.generate_destruction_trigger_rule(trigger_zone)
else:
self.generate_on_dead_trigger_rule(trigger_zone)
self.unit_map.add_scenery(scenery, trigger_zone)
def generate_destruction_trigger_rule(self, trigger_zone: TriggerZone) -> None:
# Add destruction zone trigger
t = TriggerStart(comment="Destruction")
t.actions.append(
SceneryDestructionZone(destruction_level=100, zone=trigger_zone.id)
)
self.m.triggerrules.triggers.append(t)
def generate_on_dead_trigger_rule(self, trigger_zone: TriggerZone) -> None:
# Add a TriggerRule with the MapObjectIsDead condition to recognize killed
# map objects and add them to the state.json with a DoScript
t = TriggerOnce(Event.NoEvent, f"MapObjectIsDead Trigger {trigger_zone.id}")
t.add_condition(MapObjectIsDead(trigger_zone.id))
script_string = String(
f'killed_ground_units[#killed_ground_units + 1] = "{trigger_zone.name}"'
)
t.actions.append(DoScript(script_string))
self.m.triggerrules.triggers.append(t)
class MissileSiteGenerator(GroundObjectGenerator):
@property
def culled(self) -> bool:
# Don't cull missile sites - their range is long enough to make them easily
# culled despite being a threat.
return False
def generate(self) -> None:
def generate(self, unique_name: bool = True) -> None:
super(MissileSiteGenerator, self).generate()
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
# TODO : Should be pre-planned ?
@ -221,148 +336,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]
return site_range
class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]):
"""Generator for building sites.
Building sites are the primary type of non-airbase objective locations that
appear on the map. They come in a handful of variants each with different
types of buildings and ground units.
"""
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
if self.ground_object.dcs_identifier in warehouse_map:
static_type = warehouse_map[self.ground_object.dcs_identifier]
self.generate_static(static_type)
elif self.ground_object.dcs_identifier in fortification_map:
static_type = fortification_map[self.ground_object.dcs_identifier]
self.generate_static(static_type)
elif self.ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
for f in FORTIFICATION_UNITS:
if f.id == self.ground_object.dcs_identifier:
unit_type = f
self.generate_vehicle_group(unit_type)
break
else:
logging.error(
f"{self.ground_object.dcs_identifier} not found in static maps"
)
def generate_vehicle_group(self, unit_type: Type[VehicleType]) -> None:
if not self.ground_object.is_dead:
group = self.m.vehicle_group(
country=self.country,
name=self.ground_object.group_name,
_type=unit_type,
position=self.ground_object.position,
heading=self.ground_object.heading.degrees,
)
self._register_fortification(group)
def generate_static(self, static_type: Type[StaticType]) -> None:
group = self.m.static_group(
country=self.country,
name=self.ground_object.group_name,
_type=static_type,
position=self.ground_object.position,
heading=self.ground_object.heading.degrees,
dead=self.ground_object.is_dead,
)
self._register_building(group)
def _register_fortification(self, fortification: VehicleGroup) -> None:
assert isinstance(self.ground_object, BuildingGroundObject)
self.unit_map.add_fortification(self.ground_object, fortification)
def _register_building(self, building: StaticGroup) -> None:
assert isinstance(self.ground_object, BuildingGroundObject)
self.unit_map.add_building(self.ground_object, building)
class FactoryGenerator(BuildingSiteGenerator):
"""Generator for factory sites.
Factory sites are the buildings that allow the recruitment of ground units.
Destroying these prevents the owner from recruiting ground units at the connected
control point.
"""
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
# TODO: Faction specific?
self.generate_static(Fortification.Workshop_A)
class SceneryGenerator(BuildingSiteGenerator):
def generate(self) -> None:
assert isinstance(self.ground_object, SceneryGroundObject)
trigger_zone = self.generate_trigger_zone(self.ground_object)
# DCS only visually shows a scenery object is dead when
# this trigger rule is applied. Otherwise you can kill a
# structure twice.
if self.ground_object.is_dead:
self.generate_destruction_trigger_rule(trigger_zone)
else:
self.generate_on_dead_trigger_rule(trigger_zone)
# Tell Liberation to manage this groundobjectsgen as part of the campaign.
self.register_scenery()
def generate_trigger_zone(self, scenery: SceneryGroundObject) -> TriggerZone:
zone = scenery.zone
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
if scenery.is_friendly(to_player=True):
color = {1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
else:
color = {1: 1, 2: 0.2, 3: 0.2, 4: 0.15}
# Create the smallest valid size trigger zone (16 feet) so that risk of overlap
# is minimized. As long as the triggerzone is over the scenery object, we're ok.
smallest_valid_radius = feet(16).meters
return self.m.triggers.add_triggerzone(
zone.position,
smallest_valid_radius,
zone.hidden,
zone.name,
color,
zone.properties,
)
def generate_destruction_trigger_rule(self, trigger_zone: TriggerZone) -> None:
# Add destruction zone trigger
t = TriggerStart(comment="Destruction")
t.actions.append(
SceneryDestructionZone(destruction_level=100, zone=trigger_zone.id)
)
self.m.triggerrules.triggers.append(t)
def generate_on_dead_trigger_rule(self, trigger_zone: TriggerZone) -> None:
# Add a TriggerRule with the MapObjectIsDead condition to recognize killed
# map objects and add them to the state.json with a DoScript
t = TriggerOnce(Event.NoEvent, f"MapObjectIsDead Trigger {trigger_zone.id}")
t.add_condition(MapObjectIsDead(trigger_zone.id))
script_string = String(
f'killed_ground_units[#killed_ground_units + 1] = "{trigger_zone.name}"'
)
t.actions.append(DoScript(script_string))
self.m.triggerrules.triggers.append(t)
def register_scenery(self) -> None:
scenery = self.ground_object
assert isinstance(scenery, SceneryGroundObject)
self.unit_map.add_scenery(scenery)
class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundObject]):
class GenericCarrierGenerator(GroundObjectGenerator):
"""Base type for carrier group generation.
Used by both CV(N) groups and LHA groups.
@ -389,67 +363,73 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
self.icls_alloc = icls_alloc
self.runways = runways
def generate(self) -> None:
# TODO: Require single group?
for group in self.ground_object.groups:
def generate(self, unique_name: bool = True) -> None:
# This can also be refactored as the general generation was updated
atc = self.radio_registry.alloc_uhf()
for g_id, group in enumerate(self.ground_object.groups):
if not group.units:
logging.warning(f"Found empty carrier group in {self.control_point}")
continue
atc = self.radio_registry.alloc_uhf()
ship_group = self.configure_carrier(group, atc)
for unit in group.units[1:]:
ship_group.add_unit(self.create_ship(unit, atc))
tacan = self.tacan_registry.alloc_for_band(
TacanBand.X, TacanUsage.TransmitReceive
# Correct unit type for the carrier.
# This is only used for the super carrier setting
unit_type = (
self.get_carrier_type(group)
if g_id == 0
else ship_map[group.units[0].type]
)
tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc)
ship_group = self.m.ship_group(
self.country,
group.name,
unit_type,
position=group.units[0].position,
heading=group.units[0].position.heading.degrees,
)
ship_group.set_frequency(atc.hertz)
ship_group.units[0].name = (
group.units[0].unit_name if unique_name else group.units[0].name
)
self._register_ground_unit(group.units[0], ship_group.units[0])
for unit in group.units[1:]:
ship = Ship(
self.m.next_unit_id(),
unit.unit_name if unique_name else unit.name,
unit_type_from_name(unit.type),
)
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.position.heading.degrees
ship.set_frequency(atc.hertz)
ship_group.add_unit(ship)
self._register_ground_unit(unit, ship)
# Always steam into the wind, even if the carrier is being moved.
# There are multiple unsimulated hours between turns, so we can
# count those as the time the carrier uses to move and the mission
# time as the recovery window.
brc = self.steam_into_wind(ship_group)
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
)
self._register_unit_group(group, ship_group)
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
# Set Carrier Specific Options
if g_id == 0:
tacan = self.tacan_registry.alloc_for_band(
TacanBand.X, TacanUsage.TransmitReceive
)
tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc)
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
self.add_runway_data(
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
)
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
return ship_map[group.units[0].type]
def configure_carrier(
self, group: ShipGroup, atc_channel: RadioFrequency
) -> ShipGroup:
unit_type = self.get_carrier_type(group)
ship_group = self.m.ship_group(
self.country,
group.name,
unit_type,
position=group.position,
heading=group.units[0].heading,
)
ship_group.set_frequency(atc_channel.hertz)
ship_group.units[0].name = group.units[0].name
return ship_group
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
ship = Ship(
self.m.next_unit_id(),
unit.name,
unit_type_from_name(unit.type),
)
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.heading
# TODO: Verify.
ship.set_frequency(atc_channel.hertz)
return ship
def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
wind = self.game.conditions.weather.wind.at_0m
brc = Heading.from_degrees(wind.direction).opposite
@ -515,7 +495,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
class CarrierGenerator(GenericCarrierGenerator):
"""Generator for CV(N) groups."""
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier:
unit_type = self.upgrade_to_supercarrier(unit_type, self.control_point.name)
@ -579,42 +559,6 @@ class LhaGenerator(GenericCarrierGenerator):
)
class ShipObjectGenerator(GenericGroundObjectGenerator[ShipGroundObject]):
"""Generator for non-carrier naval groups."""
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
for group in self.ground_object.groups:
if not group.units:
logging.warning(f"Found empty group in {self.ground_object}")
continue
self.generate_group(group, ship_map[group.units[0].type])
def generate_group(
self, group_def: ShipGroup, first_unit_type: Type[ShipType]
) -> None:
group = self.m.ship_group(
self.country,
group_def.name,
first_unit_type,
position=group_def.position,
heading=group_def.units[0].heading,
)
group.units[0].name = group_def.units[0].name
# TODO: Skipping the first unit looks like copy pasta from the carrier.
for unit in group_def.units[1:]:
unit_type = unit_type_from_name(unit.type)
ship = Ship(self.m.next_unit_id(), unit.name, unit_type)
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.heading
group.add_unit(ship)
self.set_alarm_state(group)
self._register_unit_group(group_def, group)
class HelipadGenerator:
"""
Generates helipads for given control point
@ -726,20 +670,8 @@ class TgoGenerator:
self.helipads[cp] = helipad_gen.helipads
for ground_object in cp.ground_objects:
generator: GenericGroundObjectGenerator[Any]
if isinstance(ground_object, FactoryGroundObject):
generator = FactoryGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
elif isinstance(ground_object, SceneryGroundObject):
generator = SceneryGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
elif isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
elif isinstance(ground_object, CarrierGroundObject):
generator: GroundObjectGenerator
if isinstance(ground_object, CarrierGroundObject):
generator = CarrierGenerator(
ground_object,
cp,
@ -753,7 +685,7 @@ class TgoGenerator:
self.unit_map,
)
elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator(
generator = LhaGenerator(
ground_object,
cp,
country,
@ -765,16 +697,12 @@ class TgoGenerator:
self.runways,
self.unit_map,
)
elif isinstance(ground_object, ShipGroundObject):
generator = ShipObjectGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
elif isinstance(ground_object, MissileSiteGroundObject):
generator = MissileSiteGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
else:
generator = GenericGroundObjectGenerator(
generator = GroundObjectGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
generator.generate()

View File

@ -11,9 +11,7 @@ if TYPE_CHECKING:
class DefendingSam(FrozenCombat):
def __init__(
self, flight: Flight, air_defenses: list[TheaterGroundObject[Any]]
) -> None:
def __init__(self, flight: Flight, air_defenses: list[TheaterGroundObject]) -> None:
super().__init__()
self.flight = flight
self.air_defenses = air_defenses

View File

@ -17,7 +17,7 @@ class SamEngagementZones:
def __init__(
self,
threat_zones: ThreatPoly,
individual_zones: list[tuple[TheaterGroundObject[Any], ThreatPoly]],
individual_zones: list[tuple[TheaterGroundObject, ThreatPoly]],
) -> None:
self.threat_zones = threat_zones
self.individual_zones = individual_zones
@ -25,9 +25,7 @@ class SamEngagementZones:
def covers(self, position: Point) -> bool:
return self.threat_zones.intersects(dcs_to_shapely_point(position))
def iter_threatening_sams(
self, position: Point
) -> Iterator[TheaterGroundObject[Any]]:
def iter_threatening_sams(self, position: Point) -> Iterator[TheaterGroundObject]:
for tgo, zone in self.individual_zones:
if zone.intersects(dcs_to_shapely_point(position)):
yield tgo
@ -44,7 +42,7 @@ class SamEngagementZones:
return SamEngagementZones(unary_union(commit_regions), individual_zones)
@classmethod
def threat_region(cls, tgo: TheaterGroundObject[Any]) -> Optional[ThreatPoly]:
def threat_region(cls, tgo: TheaterGroundObject) -> Optional[ThreatPoly]:
threat_range = tgo.max_threat_range()
if threat_range <= meters(0):
return None

View File

@ -29,8 +29,7 @@ class MissionResultsProcessor:
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_ground_losses(debriefing)
self.commit_damaged_runways(debriefing)
self.commit_captures(debriefing)
self.commit_front_line_battle_impact(debriefing)
@ -131,23 +130,11 @@ class MissionResultsProcessor:
)
@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}",
)
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:

View File

@ -25,6 +25,7 @@ from .landmap import Landmap, load_landmap, poly_contains
from .latlon import LatLon
from .projections import TransverseMercator
from .seasonalconditions import SeasonalConditions
from ..utils import Heading
if TYPE_CHECKING:
from .controlpoint import ControlPoint, MissionTarget
@ -85,7 +86,7 @@ class ConflictTheater:
def find_ground_objects_by_obj_name(
self, obj_name: str
) -> list[TheaterGroundObject[Any]]:
) -> list[TheaterGroundObject]:
found = []
for cp in self.controlpoints:
for g in cp.ground_objects:
@ -265,6 +266,29 @@ class ConflictTheater:
x, y = self.ll_to_point_transformer.transform(ll.lat, ll.lng)
return Point(x, y)
def heading_to_conflict_from(self, position: Point) -> Optional[Heading]:
# Heading for a Group to the enemy.
# Should be the point between the nearest and the most distant conflict
conflicts: dict[MissionTarget, float] = {}
for conflict in self.conflicts():
conflicts[conflict] = conflict.position.distance_to_point(position)
if len(conflicts) == 0:
return None
sorted_conflicts = [
k for k, v in sorted(conflicts.items(), key=lambda item: item[1])
]
last = len(sorted_conflicts) - 1
conflict_center = Point(
(sorted_conflicts[0].position.x + sorted_conflicts[last].position.x) / 2,
(sorted_conflicts[0].position.y + sorted_conflicts[last].position.y) / 2,
)
return Heading.from_degrees(position.heading_between_point(conflict_center))
class CaucasusTheater(ConflictTheater):
terrain = caucasus.Caucasus()

View File

@ -42,6 +42,10 @@ from .theatergroundobject import (
BuildingGroundObject,
GenericCarrierGroundObject,
TheaterGroundObject,
BuildingGroundObject,
CarrierGroundObject,
LhaGroundObject,
GroundUnit,
)
from ..ato.starttype import StartType
from ..dcs.aircrafttype import AircraftType
@ -306,7 +310,7 @@ class ControlPoint(MissionTarget, ABC):
self.full_name = name
self.at = at
self.starts_blue = starts_blue
self.connected_objectives: List[TheaterGroundObject[Any]] = []
self.connected_objectives: List[TheaterGroundObject] = []
self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = []
@ -345,7 +349,7 @@ class ControlPoint(MissionTarget, ABC):
return self.coalition.player
@property
def ground_objects(self) -> List[TheaterGroundObject[Any]]:
def ground_objects(self) -> List[TheaterGroundObject]:
return list(self.connected_objectives)
@property
@ -517,7 +521,7 @@ class ControlPoint(MissionTarget, ABC):
ControlPointType.LHA_GROUP,
]:
for g in self.ground_objects:
if g.dcs_identifier == "CARRIER":
if isinstance(g, CarrierGroundObject):
for group in g.groups:
for u in group.units:
if unit_type_from_name(u.type) in [
@ -526,7 +530,7 @@ class ControlPoint(MissionTarget, ABC):
KUZNECOW,
]:
return group.name
elif g.dcs_identifier == "LHA":
elif isinstance(g, LhaGroundObject):
for group in g.groups:
for u in group.units:
if unit_type_from_name(u.type) in [LHA_Tarawa]:
@ -539,7 +543,7 @@ class ControlPoint(MissionTarget, ABC):
def find_ground_objects_by_obj_name(
self, obj_name: str
) -> list[TheaterGroundObject[Any]]:
) -> list[TheaterGroundObject]:
found = []
for g in self.ground_objects:
if g.obj_name == obj_name:
@ -857,7 +861,7 @@ class ControlPoint(MissionTarget, ABC):
return len([obj for obj in self.connected_objectives if obj.category == "fuel"])
@property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
def strike_targets(self) -> list[GroundUnit]:
return []
@property
@ -1000,10 +1004,7 @@ class NavalControlPoint(ControlPoint, ABC):
def find_main_tgo(self) -> GenericCarrierGroundObject:
for g in self.ground_objects:
if isinstance(g, GenericCarrierGroundObject) and g.dcs_identifier in [
"CARRIER",
"LHA",
]:
if isinstance(g, GenericCarrierGroundObject):
return g
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")

View File

@ -8,6 +8,7 @@ from dcs.unit import Unit
if TYPE_CHECKING:
from game.ato.flighttype import FlightType
from game.theater.theatergroundobject import GroundUnit
class MissionTarget:
@ -46,5 +47,5 @@ class MissionTarget:
]
@property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
def strike_targets(self) -> list[GroundUnit]:
return []

View File

@ -4,19 +4,23 @@ import itertools
import logging
from abc import ABC
from collections.abc import Sequence
from typing import Generic, Iterator, List, TYPE_CHECKING, TypeVar, Union
from dataclasses import dataclass
from enum import Enum
from typing import Iterator, List, TYPE_CHECKING, Union, Optional
from dcs.mapping import Point
from dcs.triggers import TriggerZone
from dcs.unit import Unit
from dcs.unitgroup import ShipGroup, VehicleGroup
from dcs.vehicles import vehicle_map
from game.dcs.helpers import unit_type_from_name
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
from ..dcs.groundunittype import GroundUnitType
from ..point_with_heading import PointWithHeading
from ..utils import Distance, Heading, meters
if TYPE_CHECKING:
from gen.templates import UnitTemplate, GroupTemplate, TemplateRandomizer
from .controlpoint import ControlPoint
from ..ato.flighttype import FlightType
@ -45,56 +49,187 @@ NAME_BY_CATEGORY = {
}
GroupT = TypeVar("GroupT", ShipGroup, VehicleGroup)
class SkynetRole(Enum):
#: A radar SAM that should be controlled by Skynet.
Sam = "Sam"
#: A radar SAM that should be controlled and used as an EWR by Skynet.
SamAsEwr = "SamAsEwr"
#: An air defense unit that should be used as point defense by Skynet.
PointDefense = "PD"
#: All other types of groups that might be present in a SAM TGO. This includes
#: SHORADS, AAA, supply trucks, etc. Anything that shouldn't be controlled by Skynet
#: should use this role.
NoSkynetBehavior = "NoSkynetBehavior"
class TheaterGroundObject(MissionTarget, Generic[GroupT]):
class AirDefenseRange(Enum):
AAA = ("AAA", SkynetRole.NoSkynetBehavior)
Short = ("short", SkynetRole.NoSkynetBehavior)
Medium = ("medium", SkynetRole.Sam)
Long = ("long", SkynetRole.SamAsEwr)
def __init__(self, description: str, default_role: SkynetRole) -> None:
self.range_name = description
self.default_role = default_role
@dataclass
class GroundUnit:
# Units can be everything.. Static, Vehicle, Ship.
id: int
name: str
type: str # dcs.UnitType as string
position: PointWithHeading
ground_object: TheaterGroundObject
alive: bool = True
_dcs_type: Optional[GroundUnitType] = None
@staticmethod
def from_template(id: int, t: UnitTemplate, go: TheaterGroundObject) -> GroundUnit:
return GroundUnit(
id,
t.name,
t.type,
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
go,
)
@property
def unit_type(self) -> Optional[GroundUnitType]:
if not self._dcs_type:
try:
if self.type in vehicle_map:
dcs_unit_type = vehicle_map[self.type]
else:
return None
self._dcs_type = next(GroundUnitType.for_dcs_type(dcs_unit_type))
except StopIteration:
return None
return self._dcs_type
def kill(self) -> None:
self.alive = False
@property
def unit_name(self) -> str:
return f"{str(self.id).zfill(4)} | {self.name}"
@property
def display_name(self) -> str:
dead_label = " [DEAD]" if not self.alive else ""
unit_label = self.unit_type or self.type or self.name
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
class SceneryGroundUnit(GroundUnit):
"""Special GroundUnit for handling scenery ground objects"""
zone: TriggerZone
@dataclass
class GroundGroup:
id: int
name: str
position: PointWithHeading
units: list[GroundUnit]
ground_object: TheaterGroundObject
static_group: bool = False
@staticmethod
def from_template(
id: int,
g: GroupTemplate,
go: TheaterGroundObject,
randomization: bool = True,
) -> GroundGroup:
units = []
if g.randomizer:
g.randomizer.randomize()
for u_id, unit in enumerate(g.units):
tgo_unit = GroundUnit.from_template(u_id, unit, go)
if randomization and g.randomizer:
if g.randomizer.unit_type:
tgo_unit.type = g.randomizer.unit_type
try:
# Check if unit can be assigned
g.randomizer.use_unit()
except IndexError:
# Do not generate the unit as no more units are available
continue
units.append(tgo_unit)
tgo_group = GroundGroup(
id,
g.name,
PointWithHeading.from_point(go.position, go.heading),
units,
go,
)
tgo_group.static_group = g.static
return tgo_group
@property
def group_name(self) -> str:
return f"{str(self.id).zfill(4)} | {self.name}"
@property
def alive_units(self) -> int:
return sum([unit.alive for unit in self.units])
class TheaterGroundObject(MissionTarget):
def __init__(
self,
name: str,
category: str,
group_id: int,
position: Point,
heading: Heading,
control_point: ControlPoint,
dcs_identifier: str,
sea_object: bool,
) -> None:
super().__init__(name, position)
self.category = category
self.group_id = group_id
self.heading = heading
self.control_point = control_point
self.dcs_identifier = dcs_identifier
self.sea_object = sea_object
self.groups: List[GroupT] = []
self.groups: List[GroundGroup] = []
@property
def is_dead(self) -> bool:
return self.alive_unit_count == 0
@property
def units(self) -> List[Unit]:
def units(self) -> Iterator[GroundUnit]:
"""
:return: all the units at this location
"""
return list(itertools.chain.from_iterable([g.units for g in self.groups]))
yield from itertools.chain.from_iterable([g.units for g in self.groups])
@property
def dead_units(self) -> List[Unit]:
def statics(self) -> Iterator[GroundUnit]:
for group in self.groups:
if group.static_group:
yield from group.units
@property
def dead_units(self) -> list[GroundUnit]:
"""
:return: all the dead units at this location
"""
return list(
itertools.chain.from_iterable(
[getattr(g, "units_losts", []) for g in self.groups]
)
)
return [unit for unit in self.units if not unit.alive]
@property
def group_name(self) -> str:
"""The name of the unit group."""
return f"{self.category}|{self.group_id}"
return f"{self.category}|{self.name}"
@property
def waypoint_name(self) -> str:
@ -103,9 +238,6 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
def __str__(self) -> str:
return NAME_BY_CATEGORY[self.category]
def is_same_group(self, identifier: str) -> bool:
return self.group_id == identifier
@property
def obj_name(self) -> str:
return self.name
@ -135,7 +267,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
@property
def alive_unit_count(self) -> int:
return sum(len(g.units) for g in self.groups)
return sum([g.alive_units for g in self.groups])
@property
def might_have_aa(self) -> bool:
@ -149,7 +281,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
return True
return False
def _max_range_of_type(self, group: GroupT, range_type: str) -> Distance:
def _max_range_of_type(self, group: GroundGroup, range_type: str) -> Distance:
if not self.might_have_aa:
return meters(0)
@ -170,7 +302,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
def max_detection_range(self) -> Distance:
return max(self.detection_range(g) for g in self.groups)
def detection_range(self, group: GroupT) -> Distance:
def detection_range(self, group: GroundGroup) -> Distance:
return self._max_range_of_type(group, "detection_range")
def max_threat_range(self) -> Distance:
@ -178,7 +310,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
max(self.threat_range(g) for g in self.groups) if self.groups else meters(0)
)
def threat_range(self, group: GroupT, radar_only: bool = False) -> Distance:
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
return self._max_range_of_type(group, "threat_range")
@property
@ -195,8 +327,8 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
return False
@property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
return self.units
def strike_targets(self) -> list[GroundUnit]:
return [unit for unit in self.units if unit.alive]
@property
def mark_locations(self) -> Iterator[Point]:
@ -214,70 +346,31 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
raise NotImplementedError
class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
class BuildingGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
category: str,
group_id: int,
object_id: int,
position: Point,
heading: Heading,
control_point: ControlPoint,
dcs_identifier: str,
is_fob_structure: bool = False,
) -> None:
super().__init__(
name=name,
category=category,
group_id=group_id,
position=position,
heading=heading,
control_point=control_point,
dcs_identifier=dcs_identifier,
sea_object=False,
)
self.is_fob_structure = is_fob_structure
self.object_id = object_id
# Other TGOs track deadness based on the number of alive units, but
# buildings don't have groups assigned to the TGO.
self._dead = False
@property
def group_name(self) -> str:
"""The name of the unit group."""
return f"{self.category}|{self.group_id}|{self.object_id}"
@property
def waypoint_name(self) -> str:
return f"{super().waypoint_name} #{self.object_id}"
@property
def is_dead(self) -> bool:
if not hasattr(self, "_dead"):
self._dead = False
return self._dead
def kill(self) -> None:
self._dead = True
def iter_building_group(self) -> Iterator[BuildingGroundObject]:
for tgo in self.control_point.ground_objects:
if (
tgo.obj_name == self.obj_name
and not tgo.is_dead
and isinstance(tgo, BuildingGroundObject)
):
yield tgo
@property
def strike_targets(self) -> List[BuildingGroundObject]:
return list(self.iter_building_group())
@property
def mark_locations(self) -> Iterator[Point]:
for building in self.iter_building_group():
yield building.position
# Special handling to mark all buildings of the TGO
for unit in self.strike_targets:
yield unit.position
@property
def is_control_point(self) -> bool:
@ -298,55 +391,7 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
return meters(0)
class SceneryGroundObject(BuildingGroundObject):
def __init__(
self,
name: str,
category: str,
group_id: int,
object_id: int,
position: Point,
control_point: ControlPoint,
dcs_identifier: str,
zone: TriggerZone,
) -> None:
super().__init__(
name=name,
category=category,
group_id=group_id,
object_id=object_id,
position=position,
heading=Heading.from_degrees(0),
control_point=control_point,
dcs_identifier=dcs_identifier,
is_fob_structure=False,
)
self.zone = zone
class FactoryGroundObject(BuildingGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
heading: Heading,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="factory",
group_id=group_id,
object_id=0,
position=position,
heading=heading,
control_point=control_point,
dcs_identifier="Workshop A",
is_fob_structure=False,
)
class NavalGroundObject(TheaterGroundObject[ShipGroup]):
class NavalGroundObject(TheaterGroundObject):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from game.ato import FlightType
@ -375,15 +420,13 @@ class GenericCarrierGroundObject(NavalGroundObject):
# TODO: Why is this both a CP and a TGO?
class CarrierGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
def __init__(self, name: str, control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="CARRIER",
group_id=group_id,
position=control_point.position,
heading=Heading.from_degrees(0),
control_point=control_point,
dcs_identifier="CARRIER",
sea_object=True,
)
@ -399,15 +442,13 @@ class CarrierGroundObject(GenericCarrierGroundObject):
# TODO: Why is this both a CP and a TGO?
class LhaGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int, control_point: ControlPoint) -> None:
def __init__(self, name: str, control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="LHA",
group_id=group_id,
position=control_point.position,
heading=Heading.from_degrees(0),
control_point=control_point,
dcs_identifier="LHA",
sea_object=True,
)
@ -421,18 +462,14 @@ class LhaGroundObject(GenericCarrierGroundObject):
return f"LHA {self.name}"
class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
class MissileSiteGroundObject(TheaterGroundObject):
def __init__(self, name: str, position: Point, control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="missile",
group_id=group_id,
position=position,
heading=Heading.from_degrees(0),
control_point=control_point,
dcs_identifier="AA",
sea_object=False,
)
@ -445,11 +482,10 @@ class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
return False
class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
class CoastalSiteGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
heading: Heading,
@ -457,11 +493,9 @@ class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
super().__init__(
name=name,
category="coastal",
group_id=group_id,
position=position,
heading=heading,
control_point=control_point,
dcs_identifier="AA",
sea_object=False,
)
@ -474,7 +508,7 @@ class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
return False
class IadsGroundObject(TheaterGroundObject[VehicleGroup], ABC):
class IadsGroundObject(TheaterGroundObject, ABC):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from game.ato import FlightType
@ -490,18 +524,15 @@ class SamGroundObject(IadsGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="aa",
group_id=group_id,
position=position,
heading=Heading.from_degrees(0),
control_point=control_point,
dcs_identifier="AA",
sea_object=False,
)
@ -521,7 +552,7 @@ class SamGroundObject(IadsGroundObject):
def might_have_aa(self) -> bool:
return True
def threat_range(self, group: VehicleGroup, radar_only: bool = False) -> Distance:
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
max_non_radar = meters(0)
live_trs = set()
max_telar_range = meters(0)
@ -554,22 +585,19 @@ class SamGroundObject(IadsGroundObject):
return True
class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]):
class VehicleGroupGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="armor",
group_id=group_id,
position=position,
heading=Heading.from_degrees(0),
control_point=control_point,
dcs_identifier="AA",
sea_object=False,
)
@ -586,18 +614,15 @@ class EwrGroundObject(IadsGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="ewr",
group_id=group_id,
position=position,
heading=Heading.from_degrees(0),
control_point=control_point,
dcs_identifier="EWR",
sea_object=False,
)
@ -605,7 +630,7 @@ class EwrGroundObject(IadsGroundObject):
def group_name(self) -> str:
# Prefix the group names with the side color so Skynet can find them.
# Use Group Id and uppercase EWR
return f"{self.faction_color}|EWR|{self.group_id}"
return f"{self.faction_color}|EWR|{self.name}"
@property
def might_have_aa(self) -> bool:
@ -621,17 +646,13 @@ class EwrGroundObject(IadsGroundObject):
class ShipGroundObject(NavalGroundObject):
def __init__(
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
def __init__(self, name: str, position: Point, control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="ship",
group_id=group_id,
position=position,
heading=Heading.from_degrees(0),
control_point=control_point,
dcs_identifier="AA",
sea_object=True,
)

View File

@ -208,7 +208,7 @@ class ThreatZones:
cls,
doctrine: Doctrine,
barcap_locations: Iterable[ControlPoint],
air_defenses: Iterable[TheaterGroundObject[Any]],
air_defenses: Iterable[TheaterGroundObject],
) -> ThreatZones:
"""Generates the threat zones projected by the given locations.

View File

@ -4,16 +4,17 @@ from __future__ import annotations
import itertools
import math
from dataclasses import dataclass
from typing import Dict, Optional, Any, TYPE_CHECKING, Union, TypeVar, Generic
from typing import Dict, Optional, Any, TYPE_CHECKING
from dcs.unit import Vehicle, Ship
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup
from dcs.triggers import TriggerZone
from dcs.unit import Unit
from dcs.unitgroup import FlyingGroup, VehicleGroup, ShipGroup
from game.dcs.groundunittype import GroundUnitType
from game.squadrons import Pilot
from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
from game.theater import Airfield, ControlPoint, GroundUnit
from game.ato.flight import Flight
from game.theater.theatergroundobject import SceneryGroundUnit
if TYPE_CHECKING:
from game.transfers import CargoShip, Convoy, TransferOrder
@ -31,14 +32,16 @@ class FrontLineUnit:
origin: ControlPoint
UnitT = TypeVar("UnitT", Ship, Vehicle)
@dataclass(frozen=True)
class GroundObjectMapping:
ground_unit: GroundUnit
dcs_unit: Unit
@dataclass(frozen=True)
class GroundObjectUnit(Generic[UnitT]):
ground_object: TheaterGroundObject[Any]
group: MovingGroup[UnitT]
unit: UnitT
class SceneryObjectMapping:
ground_unit: GroundUnit
trigger_zone: TriggerZone
@dataclass(frozen=True)
@ -53,18 +56,13 @@ class AirliftUnits:
transfer: TransferOrder
@dataclass(frozen=True)
class Building:
ground_object: BuildingGroundObject
class UnitMap:
def __init__(self) -> None:
self.aircraft: Dict[str, FlyingUnit] = {}
self.airfields: Dict[str, Airfield] = {}
self.front_line_units: Dict[str, FrontLineUnit] = {}
self.ground_object_units: Dict[str, GroundObjectUnit[Any]] = {}
self.buildings: Dict[str, Building] = {}
self.ground_objects: Dict[str, GroundObjectMapping] = {}
self.scenery_objects: Dict[str, SceneryObjectMapping] = {}
self.convoys: Dict[str, ConvoyUnit] = {}
self.cargo_ships: Dict[str, CargoShip] = {}
self.airlifts: Dict[str, AirliftUnits] = {}
@ -105,41 +103,18 @@ class UnitMap:
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
return self.front_line_units.get(name, None)
def add_ground_object_units(
self,
ground_object: TheaterGroundObject[Any],
persistence_group: Union[ShipGroup, VehicleGroup],
miz_group: Union[ShipGroup, VehicleGroup],
def add_ground_object_mapping(
self, ground_unit: GroundUnit, dcs_unit: Unit
) -> None:
"""Adds a group associated with a TGO to the unit map.
# Deaths for units at TGOs are recorded in the corresponding GroundUnit within
# the GroundGroup, so we have to match the dcs unit with the liberation unit
name = str(dcs_unit.name)
if name in self.ground_objects:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.ground_objects[name] = GroundObjectMapping(ground_unit, dcs_unit)
Args:
ground_object: The TGO the group is associated with.
persistence_group: The Group tracked by the TGO itself.
miz_group: The Group spawned for the miz to match persistence_group.
"""
# Deaths for units at TGOs are recorded in the Group that is contained
# by the TGO, but when groundobjectsgen populates the miz it creates new
# groups based on that template, so the units and groups in the miz are
# not a direct match for the units and groups that persist in the TGO.
#
# This means that we need to map the spawned unit names back to the
# original TGO units, not the ones in the miz.
if len(persistence_group.units) != len(miz_group.units):
raise ValueError("Persistent group does not match generated group")
unit_pairs = zip(persistence_group.units, miz_group.units)
for persistent_unit, miz_unit in unit_pairs:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(miz_unit.name)
if name in self.ground_object_units:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.ground_object_units[name] = GroundObjectUnit(
ground_object, persistence_group, persistent_unit
)
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit[Any]]:
return self.ground_object_units.get(name, None)
def ground_object(self, name: str) -> Optional[GroundObjectMapping]:
return self.ground_objects.get(name, None)
def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None:
for unit, unit_type in zip(group.units, convoy.iter_units()):
@ -195,40 +170,13 @@ class UnitMap:
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
return self.airlifts.get(name, None)
def add_building(
self, ground_object: BuildingGroundObject, group: StaticGroup
def add_scenery(
self, scenery_unit: SceneryGroundUnit, trigger_zone: TriggerZone
) -> None:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
# The name of the initiator in the DCS dead event will have " object"
# appended for statics.
name = f"{group.name} object"
if name in self.buildings:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object)
name = str(trigger_zone.name)
if name in self.scenery_objects:
raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)")
self.scenery_objects[name] = SceneryObjectMapping(scenery_unit, trigger_zone)
def add_fortification(
self, ground_object: BuildingGroundObject, group: VehicleGroup
) -> None:
if len(group.units) != 1:
raise ValueError("Fortification groups must have exactly one unit.")
unit = group.units[0]
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.buildings:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object)
def add_scenery(self, ground_object: SceneryGroundObject) -> None:
name = str(ground_object.zone.name)
if name in self.buildings:
raise RuntimeError(
f"Duplicate TGO unit: {name}. TriggerZone name: "
f"{ground_object.dcs_identifier}"
)
self.buildings[name] = Building(ground_object)
def building_or_fortification(self, name: str) -> Optional[Building]:
return self.buildings.get(name, None)
def scenery_object(self, name: str) -> Optional[SceneryObjectMapping]:
return self.scenery_objects.get(name, None)

View File

@ -46,9 +46,9 @@ from game.theater import (
TheaterGroundObject,
)
from game.theater.theatergroundobject import (
BuildingGroundObject,
EwrGroundObject,
NavalGroundObject,
GroundUnit,
)
from game.typeguard import self_type_guard
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
@ -1086,7 +1086,7 @@ class FlightPlanBuilder:
self,
flight: Flight,
# TODO: Custom targets should be an attribute of the flight.
custom_targets: Optional[List[Unit]] = None,
custom_targets: Optional[List[GroundUnit]] = None,
) -> None:
"""Creates a default flight plan for the given mission."""
if flight not in self.package.flights:
@ -1106,7 +1106,7 @@ class FlightPlanBuilder:
) from ex
def generate_flight_plan(
self, flight: Flight, custom_targets: Optional[List[Unit]]
self, flight: Flight, custom_targets: Optional[List[GroundUnit]]
) -> FlightPlan:
# TODO: Flesh out mission types.
task = flight.flight_type
@ -1207,16 +1207,9 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location)
targets: List[StrikeTarget] = []
if isinstance(location, BuildingGroundObject):
# A building "group" is implemented as multiple TGOs with the same name.
for building in location.strike_targets:
targets.append(StrikeTarget(building.category, building))
else:
# TODO: Replace with DEAD?
# Strike missions on SEAD targets target units.
for g in location.groups:
for j, u in enumerate(g.units):
targets.append(StrikeTarget(f"{u.type} #{j}", u))
for j, u in enumerate(location.strike_targets):
targets.append(StrikeTarget(f"{u.type} #{j}", u))
return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_STRIKE, targets
@ -1675,7 +1668,7 @@ class FlightPlanBuilder:
)
def generate_dead(
self, flight: Flight, custom_targets: Optional[List[Unit]]
self, flight: Flight, custom_targets: Optional[List[GroundUnit]]
) -> StrikeFlightPlan:
"""Generate a DEAD flight at a given location.
@ -1745,7 +1738,7 @@ class FlightPlanBuilder:
)
def generate_sead(
self, flight: Flight, custom_targets: Optional[List[Unit]]
self, flight: Flight, custom_targets: Optional[List[GroundUnit]]
) -> StrikeFlightPlan:
"""Generate a SEAD flight at a given location.

View File

@ -1,5 +1,4 @@
from __future__ import annotations
import logging
import random
from dataclasses import dataclass
@ -11,12 +10,9 @@ from typing import (
TYPE_CHECKING,
Tuple,
Union,
Any,
)
from dcs.mapping import Point
from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup, ShipGroup
from game.theater import (
ControlPoint,
@ -32,14 +28,13 @@ if TYPE_CHECKING:
from game.ato.flight import Flight
from game.coalition import Coalition
from game.transfers import MultiGroupTransport
from game.theater.theatergroundobject import GroundUnit, GroundGroup
@dataclass(frozen=True)
class StrikeTarget:
name: str
target: Union[
VehicleGroup, TheaterGroundObject[Any], Unit, ShipGroup, MultiGroupTransport
]
target: Union[TheaterGroundObject, GroundGroup, GroundUnit, MultiGroupTransport]
class WaypointBuilder:

View File

@ -2,6 +2,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game
from game.theater import ControlPointType, BuildingGroundObject
from game.theater.theatergroundobject import IadsGroundObject
from game.utils import Distance
from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
@ -115,8 +116,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
):
for ground_object in cp.ground_objects:
if not ground_object.is_dead and (
ground_object.dcs_identifier == "AA"
or ground_object.dcs_identifier == "EWR"
isinstance(ground_object, IadsGroundObject)
):
for g in ground_object.groups:
for j, u in enumerate(g.units):

View File

@ -1,6 +1,7 @@
from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game
from game.theater import SamGroundObject
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
@ -53,7 +54,7 @@ class QStrikeTargetSelectionComboBox(QFilteredComboBox):
target.location = g
target.name = g.obj_name
if g.dcs_identifier == "AA":
if isinstance(g, SamGroundObject):
target.name = g.obj_name + " [units]"
for group in g.groups:
for u in group.units:

View File

@ -3,11 +3,8 @@ from __future__ import annotations
from typing import List, Optional
from PySide2.QtCore import Property, QObject, Signal, Slot
from dcs.unit import Unit
from dcs.vehicles import vehicle_map
from game import Game
from game.dcs.groundunittype import GroundUnitType
from game.server.leaflet import LeafletLatLon
from game.theater import TheaterGroundObject
from qt_ui.dialogs import Dialog
@ -30,7 +27,6 @@ class GroundObjectJs(QObject):
self.tgo = tgo
self.game = game
self.theater = game.theater
self.buildings = self.theater.find_ground_objects_by_obj_name(self.tgo.obj_name)
self.dialog: Optional[QGroundObjectMenu] = None
@Slot()
@ -39,7 +35,6 @@ class GroundObjectJs(QObject):
self.dialog = QGroundObjectMenu(
None,
self.tgo,
self.buildings,
self.tgo.control_point,
self.game,
)
@ -61,38 +56,9 @@ class GroundObjectJs(QObject):
def category(self) -> str:
return self.tgo.category
@staticmethod
def make_unit_name(unit: Unit, dead: bool) -> str:
dead_label = " [DEAD]" if dead else ""
unit_display_name = unit.type
dcs_unit_type = vehicle_map.get(unit.type)
if dcs_unit_type is not None:
# TODO: Make the TGO contain GroundUnitType instead of the pydcs Group.
# This is a hack because we can't know which variant was used.
try:
unit_display_name = next(
GroundUnitType.for_dcs_type(dcs_unit_type)
).name
except StopIteration:
pass
return f"Unit #{unit.id} - {unit_display_name}{dead_label}"
@Property(list, notify=unitsChanged)
def units(self) -> List[str]:
units = []
# TGOs with a non-empty group set are non-building TGOs. Building TGOs have no
# groups set, but instead are one TGO per building "group" (DCS doesn't support
# groups of statics) all with the same name.
if self.tgo.groups:
for unit in self.tgo.units:
units.append(self.make_unit_name(unit, dead=False))
for unit in self.tgo.dead_units:
units.append(self.make_unit_name(unit, dead=True))
else:
for building in self.buildings:
dead = " [DEAD]" if building.is_dead else ""
units.append(f"{building.dcs_identifier}{dead}")
return units
return [unit.display_name for unit in self.tgo.units]
@Property(bool, notify=blueChanged)
def blue(self) -> bool:
@ -105,9 +71,7 @@ class GroundObjectJs(QObject):
@Property(bool, notify=deadChanged)
def dead(self) -> bool:
if not self.tgo.groups:
return all(b.is_dead for b in self.buildings)
return not any(g.units for g in self.tgo.groups)
return not any(g.alive_units > 0 for g in self.tgo.groups)
@Property(list, notify=samThreatRangesChanged)
def samThreatRanges(self) -> List[float]:

View File

@ -35,7 +35,8 @@ class LossGrid(QGridLayout):
self.add_loss_rows(
debriefing.airlift_losses_by_type(player), lambda u: f"{u} from airlift"
)
self.add_loss_rows(debriefing.building_losses_by_type(player), lambda u: u)
self.add_loss_rows(debriefing.ground_object_losses_by_type(player), lambda u: u)
self.add_loss_rows(debriefing.scenery_losses_by_type(player), lambda u: u)
# TODO: Display dead ground object units and runways.

View File

@ -169,12 +169,14 @@ class QWaitingForMissionResultWindow(QDialog):
update_layout,
)
self.add_update_row(
"Ground units lost at objective areas",
"Ground Objects destroyed",
len(list(debriefing.ground_object_losses)),
update_layout,
)
self.add_update_row(
"Buildings destroyed", len(list(debriefing.building_losses)), update_layout
"Scenery Objects destroyed",
len(list(debriefing.scenery_object_losses)),
update_layout,
)
self.add_update_row(
"Base capture events", len(debriefing.base_captures), update_layout

View File

@ -2,6 +2,7 @@ import os
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout
from game.theater import GroundUnit
from game.config import REWARDS
@ -9,16 +10,16 @@ from game.config import REWARDS
class QBuildingInfo(QGroupBox):
def __init__(self, building, ground_object):
super(QBuildingInfo, self).__init__()
self.building = building
self.building: GroundUnit = building
self.ground_object = ground_object
self.init_ui()
def init_ui(self):
self.header = QLabel()
path = os.path.join(
"./resources/ui/units/buildings/" + self.building.dcs_identifier + ".png"
"./resources/ui/units/buildings/" + self.building.type + ".png"
)
if self.building.is_dead:
if not self.building.alive:
pixmap = QPixmap("./resources/ui/units/buildings/dead.png")
elif os.path.isfile(path):
pixmap = QPixmap(path)
@ -26,8 +27,8 @@ class QBuildingInfo(QGroupBox):
pixmap = QPixmap("./resources/ui/units/buildings/missing.png")
self.header.setPixmap(pixmap)
name = "<b>{}</b> {}".format(
self.building.dcs_identifier[0:18],
"[DEAD]" if self.building.is_dead else "",
self.building.type[0:18],
"[DEAD]" if not self.building.alive else "",
)
self.name = QLabel(name)
self.name.setProperty("style", "small")
@ -35,9 +36,11 @@ class QBuildingInfo(QGroupBox):
layout.addWidget(self.header)
layout.addWidget(self.name)
if self.building.category in REWARDS.keys():
income_label_text = "Value: " + str(REWARDS[self.building.category]) + "M"
if self.building.is_dead:
if self.ground_object.category in REWARDS.keys():
income_label_text = (
"Value: " + str(REWARDS[self.ground_object.category]) + "M"
)
if not self.building.alive:
income_label_text = "<s>" + income_label_text + "</s>"
self.reward = QLabel(income_label_text)
layout.addWidget(self.reward)

View File

@ -107,58 +107,21 @@ class QGroundObjectMenu(QDialog):
self.intelLayout = QGridLayout()
i = 0
for g in self.ground_object.groups:
if not hasattr(g, "units_losts"):
g.units_losts = []
for unit in g.units:
unit_display_name = unit.type
dcs_unit_type = vehicles.vehicle_map.get(unit.type)
if dcs_unit_type is not None:
# Hack: Don't know which variant is used.
try:
unit_display_name = next(
GroundUnitType.for_dcs_type(dcs_unit_type)
).name
except StopIteration:
pass
self.intelLayout.addWidget(
QLabel(
"<b>Unit #"
+ str(unit.id)
+ " - "
+ str(unit_display_name)
+ "</b>"
),
i,
0,
QLabel(f"<b>Unit {str(unit.display_name)}</b>"), i, 0
)
i = i + 1
for unit in g.units_losts:
dcs_unit_type = vehicles.vehicle_map.get(unit.type)
if dcs_unit_type is None:
continue
# Hack: Don't know which variant is used.
try:
unit_type = next(GroundUnitType.for_dcs_type(dcs_unit_type))
name = unit_type.name
price = unit_type.price
except StopIteration:
name = dcs_unit_type.name
price = 0
self.intelLayout.addWidget(
QLabel(f"<b>Unit #{unit.id} - {name}</b> [DEAD]"), i, 0
)
if self.cp.captured:
if not unit.alive and self.cp.captured:
price = unit.unit_type.price if unit.unit_type else 0
repair = QPushButton(f"Repair [{price}M]")
repair.setProperty("style", "btn-success")
repair.clicked.connect(
lambda u=unit, g=g, p=unit_type.price: self.repair_unit(g, u, p)
lambda u=unit, p=price: self.repair_unit(u, p)
)
self.intelLayout.addWidget(repair, i, 1)
i = i + 1
i += 1
stretch = QVBoxLayout()
stretch.addStretch()
self.intelLayout.addLayout(stretch, i, 0)
@ -169,19 +132,19 @@ class QGroundObjectMenu(QDialog):
j = 0
total_income = 0
received_income = 0
for i, building in enumerate(self.buildings):
if building.dcs_identifier not in FORTIFICATION_BUILDINGS:
for static in self.ground_object.statics:
if static not in FORTIFICATION_BUILDINGS:
self.buildingsLayout.addWidget(
QBuildingInfo(building, self.ground_object), j / 3, j % 3
QBuildingInfo(static, self.ground_object), j / 3, j % 3
)
j = j + 1
if building.category in REWARDS.keys():
total_income = total_income + REWARDS[building.category]
if not building.is_dead:
received_income = received_income + REWARDS[building.category]
if self.ground_object.category in REWARDS.keys():
total_income += REWARDS[self.ground_object.category]
if static.alive:
received_income += REWARDS[self.ground_object.category]
else:
logging.warning(building.category + " not in REWARDS")
logging.warning(self.ground_object.category + " not in REWARDS")
self.financesBox = QGroupBox("Finances: ")
self.financesBoxLayout = QGridLayout()
@ -235,11 +198,10 @@ class QGroundObjectMenu(QDialog):
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
self.total_value = total_value
def repair_unit(self, group, unit, price):
def repair_unit(self, unit, price):
if self.game.blue.budget > price:
self.game.blue.budget -= price
group.units_losts = [u for u in group.units_losts if u.id != unit.id]
group.units.append(unit)
unit.alive = True
GameUpdateSignal.get_instance().updateGame(self.game)
# Remove destroyed units in the vicinity