diff --git a/game/commander/tasks/primitive/strike.py b/game/commander/tasks/primitive/strike.py
index e5fcec20..8bb4a9c5 100644
--- a/game/commander/tasks/primitive/strike.py
+++ b/game/commander/tasks/primitive/strike.py
@@ -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
diff --git a/game/commander/theaterstate.py b/game/commander/theaterstate.py
index 0465fe59..1b868683 100644
--- a/game/commander/theaterstate.py
+++ b/game/commander/theaterstate.py
@@ -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
diff --git a/game/data/alic.py b/game/data/alic.py
index f8bc5e43..bcdd5991 100644
--- a/game/data/alic.py
+++ b/game/data/alic.py
@@ -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]
diff --git a/game/dcs/helpers.py b/game/dcs/helpers.py
index e2b7ef72..b8bcb751 100644
--- a/game/dcs/helpers.py
+++ b/game/dcs/helpers.py
@@ -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
diff --git a/game/debriefing.py b/game/debriefing.py
index 0304951d..0029cfc9 100644
--- a/game/debriefing.py
+++ b/game/debriefing.py
@@ -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)
diff --git a/game/game.py b/game/game.py
index 51305c20..ab4adf54 100644
--- a/game/game.py
+++ b/game/game.py
@@ -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
diff --git a/game/missiongenerator/aircraft/waypoints/baiingress.py b/game/missiongenerator/aircraft/waypoints/baiingress.py
index b757b651..bd8f679d 100644
--- a/game/missiongenerator/aircraft/waypoints/baiingress.py
+++ b/game/missiongenerator/aircraft/waypoints/baiingress.py
@@ -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
diff --git a/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py b/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py
index 9d6c4861..16e8fd28 100644
--- a/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py
+++ b/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py
@@ -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):
diff --git a/game/missiongenerator/kneeboard.py b/game/missiongenerator/kneeboard.py
index 133d3c47..8a27aa10 100644
--- a/game/missiongenerator/kneeboard.py
+++ b/game/missiongenerator/kneeboard.py
@@ -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):
diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py
index a2cadb93..5dc0ad98 100644
--- a/game/missiongenerator/tgogenerator.py
+++ b/game/missiongenerator/tgogenerator.py
@@ -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()
diff --git a/game/sim/combat/defendingsam.py b/game/sim/combat/defendingsam.py
index fca901ac..108bc408 100644
--- a/game/sim/combat/defendingsam.py
+++ b/game/sim/combat/defendingsam.py
@@ -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
diff --git a/game/sim/combat/samengagementzones.py b/game/sim/combat/samengagementzones.py
index 7f58c981..0d4c9177 100644
--- a/game/sim/combat/samengagementzones.py
+++ b/game/sim/combat/samengagementzones.py
@@ -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
diff --git a/game/sim/missionresultsprocessor.py b/game/sim/missionresultsprocessor.py
index 2403fe7f..22974154 100644
--- a/game/sim/missionresultsprocessor.py
+++ b/game/sim/missionresultsprocessor.py
@@ -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:
diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py
index bdaa1007..93336835 100644
--- a/game/theater/conflicttheater.py
+++ b/game/theater/conflicttheater.py
@@ -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()
diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py
index a0c57d53..61b027ff 100644
--- a/game/theater/controlpoint.py
+++ b/game/theater/controlpoint.py
@@ -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}")
diff --git a/game/theater/missiontarget.py b/game/theater/missiontarget.py
index 5942bb0e..7a72d645 100644
--- a/game/theater/missiontarget.py
+++ b/game/theater/missiontarget.py
@@ -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 []
diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py
index 2be1c41d..6265e241 100644
--- a/game/theater/theatergroundobject.py
+++ b/game/theater/theatergroundobject.py
@@ -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,
)
diff --git a/game/threatzones.py b/game/threatzones.py
index f916bd8e..bb4f297f 100644
--- a/game/threatzones.py
+++ b/game/threatzones.py
@@ -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.
diff --git a/game/unitmap.py b/game/unitmap.py
index b73f6984..b43a1cef 100644
--- a/game/unitmap.py
+++ b/game/unitmap.py
@@ -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)
diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py
index 224440c7..43917615 100644
--- a/gen/flights/flightplan.py
+++ b/gen/flights/flightplan.py
@@ -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.
diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py
index cdee7676..58d89439 100644
--- a/gen/flights/waypointbuilder.py
+++ b/gen/flights/waypointbuilder.py
@@ -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:
diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
index f857dfd6..4a9d1435 100644
--- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
+++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py
@@ -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):
diff --git a/qt_ui/widgets/combos/QStrikeTargetSelectionComboBox.py b/qt_ui/widgets/combos/QStrikeTargetSelectionComboBox.py
index d31c501c..7074d0fb 100644
--- a/qt_ui/widgets/combos/QStrikeTargetSelectionComboBox.py
+++ b/qt_ui/widgets/combos/QStrikeTargetSelectionComboBox.py
@@ -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:
diff --git a/qt_ui/widgets/map/model/groundobjectjs.py b/qt_ui/widgets/map/model/groundobjectjs.py
index 36a278b7..7377567e 100644
--- a/qt_ui/widgets/map/model/groundobjectjs.py
+++ b/qt_ui/widgets/map/model/groundobjectjs.py
@@ -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]:
diff --git a/qt_ui/windows/QDebriefingWindow.py b/qt_ui/windows/QDebriefingWindow.py
index 20e01cc3..f658a162 100644
--- a/qt_ui/windows/QDebriefingWindow.py
+++ b/qt_ui/windows/QDebriefingWindow.py
@@ -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.
diff --git a/qt_ui/windows/QWaitingForMissionResultWindow.py b/qt_ui/windows/QWaitingForMissionResultWindow.py
index 5c07b4cb..37c64d84 100644
--- a/qt_ui/windows/QWaitingForMissionResultWindow.py
+++ b/qt_ui/windows/QWaitingForMissionResultWindow.py
@@ -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
diff --git a/qt_ui/windows/groundobject/QBuildingInfo.py b/qt_ui/windows/groundobject/QBuildingInfo.py
index 05c1b8f6..811e7270 100644
--- a/qt_ui/windows/groundobject/QBuildingInfo.py
+++ b/qt_ui/windows/groundobject/QBuildingInfo.py
@@ -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 = "{} {}".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 = "" + income_label_text + ""
self.reward = QLabel(income_label_text)
layout.addWidget(self.reward)
diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py
index ef4b32b9..750ee898 100644
--- a/qt_ui/windows/groundobject/QGroundObjectMenu.py
+++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py
@@ -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(
- "Unit #"
- + str(unit.id)
- + " - "
- + str(unit_display_name)
- + ""
- ),
- i,
- 0,
+ QLabel(f"Unit {str(unit.display_name)}"), 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"Unit #{unit.id} - {name} [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