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 @dataclass
class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]): class PlanStrike(PackagePlanningTask[TheaterGroundObject]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if self.target not in state.strike_targets: if self.target not in state.strike_targets:
return False return False

View File

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

View File

@ -1,4 +1,3 @@
from dcs.unit import Unit
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
@ -38,5 +37,5 @@ class AlicCodes:
} }
@classmethod @classmethod
def code_for(cls, unit: Unit) -> int: def code_for(cls, unit_type: str) -> int:
return cls.CODES[unit.type] 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.ships import ship_map
from dcs.unittype import UnitType from dcs.unittype import UnitType
from dcs.vehicles import vehicle_map 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]]: 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] return ship_map[name]
if name in helicopter_map: if name in helicopter_map:
return helicopter_map[name] 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: else:
return None return None

View File

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

View File

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

View File

@ -19,7 +19,9 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
elif isinstance(target, MultiGroupTransport): elif isinstance(target, MultiGroupTransport):
group_names.append(target.name) group_names.append(target.name)
elif isinstance(target, NavalControlPoint): 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: else:
logging.error( logging.error(
"Unexpected target type for BAI mission: %s", "Unexpected target type for BAI mission: %s",
@ -28,12 +30,12 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
return return
for group_name in group_names: for group_name in group_names:
group = self.mission.find_group(group_name) miz_group = self.mission.find_group(group_name)
if group is None: if miz_group is None:
logging.error("Could not find group for BAI mission %s", group_name) logging.error("Could not find group for BAI mission %s", group_name)
continue continue
task = AttackGroup(group.id, weapon_type=WeaponType.Auto) task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto)
task.params["attackQtyLimit"] = False task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = 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 import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.airsupport import AirSupport from game.missiongenerator.airsupport import AirSupport
from game.theater import MissionTarget from game.theater import MissionTarget, GroundUnit
TARGET_WAYPOINTS = ( TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
@ -82,7 +82,7 @@ class PydcsWaypointBuilder:
return False return False
def register_special_waypoints( def register_special_waypoints(
self, targets: Iterable[Union[MissionTarget, Unit]] self, targets: Iterable[Union[MissionTarget, GroundUnit]]
) -> None: ) -> None:
"""Create special target waypoints for various aircraft""" """Create special target waypoints for various aircraft"""
for i, t in enumerate(targets): 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 PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission from dcs.mission import Mission
from dcs.unit import Unit
from tabulate import tabulate from tabulate import tabulate
from game.ato.flighttype import FlightType from game.ato.flighttype import FlightType
@ -41,7 +40,7 @@ from game.ato.flightwaypointtype import FlightWaypointType
from game.data.alic import AlicCodes from game.data.alic import AlicCodes
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.radio.radios import RadioFrequency 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.theater.bullseye import Bullseye
from game.utils import Distance, UnitSystem, meters, mps, pounds from game.utils import Distance, UnitSystem, meters, mps, pounds
from game.weather import Weather from game.weather import Weather
@ -608,14 +607,14 @@ class SeadTaskPage(KneeboardPage):
self.theater = theater self.theater = theater
@property @property
def target_units(self) -> Iterator[Unit]: def target_units(self) -> Iterator[GroundUnit]:
if isinstance(self.flight.package.target, TheaterGroundObject): if isinstance(self.flight.package.target, TheaterGroundObject):
yield from self.flight.package.target.units yield from self.flight.package.target.strike_targets
@staticmethod @staticmethod
def alic_for(unit: Unit) -> str: def alic_for(unit_type: str) -> str:
try: try:
return str(AlicCodes.code_for(unit)) return str(AlicCodes.code_for(unit_type))
except KeyError: except KeyError:
return "" return ""
@ -635,11 +634,15 @@ class SeadTaskPage(KneeboardPage):
writer.write(path) 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) ll = self.theater.point_to_ll(unit.position)
unit_type = unit_type_from_name(unit.type) unit_type = unit_type_from_name(unit.type)
name = unit.name if unit_type is None else unit_type.name 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): class StrikeTaskPage(KneeboardPage):

View File

@ -20,6 +20,8 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Type, Type,
TypeVar, TypeVar,
List,
Any,
Union, Union,
) )
@ -49,24 +51,23 @@ from dcs.task import (
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone
from dcs.unit import InvisibleFARP, Ship, Unit, Vehicle 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.unittype import ShipType, StaticType, VehicleType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID 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.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.theater import ControlPoint, TheaterGroundObject from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject,
CarrierGroundObject, CarrierGroundObject,
FactoryGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
LhaGroundObject, LhaGroundObject,
MissileSiteGroundObject, MissileSiteGroundObject,
SceneryGroundObject, GroundGroup,
ShipGroundObject, GroundUnit,
SceneryGroundUnit,
) )
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import Heading, feet, knots, mps from game.utils import Heading, feet, knots, mps
@ -79,18 +80,12 @@ FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000 AA_CP_MIN_DISTANCE = 40000
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any]) class GroundObjectGenerator:
"""generates the DCS groups and units from the TheaterGroundObject"""
class GenericGroundObjectGenerator(Generic[TgoT]):
"""An unspecialized ground object generator.
Currently used only for SAM
"""
def __init__( def __init__(
self, self,
ground_object: TgoT, ground_object: TheaterGroundObject,
country: Country, country: Country,
game: Game, game: Game,
mission: Mission, mission: Mission,
@ -106,7 +101,7 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
def culled(self) -> bool: def culled(self) -> bool:
return self.game.iads_considerate_culling(self.ground_object) return self.game.iads_considerate_culling(self.ground_object)
def generate(self) -> None: def generate(self, unique_name: bool = True) -> None:
if self.culled: if self.culled:
return return
@ -114,29 +109,102 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
if not group.units: if not group.units:
logging.warning(f"Found empty group in {self.ground_object}") logging.warning(f"Found empty group in {self.ground_object}")
continue 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] # Only skip dead units after trigger zone for scenery created!
vg = self.m.vehicle_group( if not u.alive:
self.country, continue
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)
self.enable_eplrs(vg, unit_type) unit_type = static_type_from_name(u.type)
self.set_alarm_state(vg) if not unit_type:
self._register_unit_group(group, vg) 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 @staticmethod
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None: def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
@ -149,24 +217,71 @@ class GenericGroundObjectGenerator(Generic[TgoT]):
else: else:
group.points[0].tasks.append(OptAlarmState(1)) group.points[0].tasks.append(OptAlarmState(1))
def _register_unit_group( def _register_ground_unit(
self, self,
persistence_group: Union[ShipGroup, VehicleGroup], ground_unit: GroundUnit,
miz_group: Union[ShipGroup, VehicleGroup], dcs_unit: Unit,
) -> None: ) -> None:
self.unit_map.add_ground_object_units( self.unit_map.add_ground_object_mapping(ground_unit, dcs_unit)
self.ground_object, persistence_group, miz_group
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 @property
def culled(self) -> bool: def culled(self) -> bool:
# Don't cull missile sites - their range is long enough to make them easily # Don't cull missile sites - their range is long enough to make them easily
# culled despite being a threat. # culled despite being a threat.
return False return False
def generate(self) -> None: def generate(self, unique_name: bool = True) -> None:
super(MissileSiteGenerator, self).generate() super(MissileSiteGenerator, self).generate()
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now) # Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
# TODO : Should be pre-planned ? # TODO : Should be pre-planned ?
@ -221,148 +336,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]
return site_range return site_range
class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]): class GenericCarrierGenerator(GroundObjectGenerator):
"""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]):
"""Base type for carrier group generation. """Base type for carrier group generation.
Used by both CV(N) groups and LHA groups. Used by both CV(N) groups and LHA groups.
@ -389,67 +363,73 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
self.icls_alloc = icls_alloc self.icls_alloc = icls_alloc
self.runways = runways self.runways = runways
def generate(self) -> None: def generate(self, unique_name: bool = True) -> None:
# TODO: Require single group?
for group in self.ground_object.groups: # 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: if not group.units:
logging.warning(f"Found empty carrier group in {self.control_point}") logging.warning(f"Found empty carrier group in {self.control_point}")
continue continue
atc = self.radio_registry.alloc_uhf() # Correct unit type for the carrier.
ship_group = self.configure_carrier(group, atc) # This is only used for the super carrier setting
for unit in group.units[1:]: unit_type = (
ship_group.add_unit(self.create_ship(unit, atc)) self.get_carrier_type(group)
if g_id == 0
tacan = self.tacan_registry.alloc_for_band( else ship_map[group.units[0].type]
TacanBand.X, TacanUsage.TransmitReceive
) )
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. # Always steam into the wind, even if the carrier is being moved.
# There are multiple unsimulated hours between turns, so we can # There are multiple unsimulated hours between turns, so we can
# count those as the time the carrier uses to move and the mission # count those as the time the carrier uses to move and the mission
# time as the recovery window. # time as the recovery window.
brc = self.steam_into_wind(ship_group) 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] 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]: def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
wind = self.game.conditions.weather.wind.at_0m wind = self.game.conditions.weather.wind.at_0m
brc = Heading.from_degrees(wind.direction).opposite brc = Heading.from_degrees(wind.direction).opposite
@ -515,7 +495,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
class CarrierGenerator(GenericCarrierGenerator): class CarrierGenerator(GenericCarrierGenerator):
"""Generator for CV(N) groups.""" """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) unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier: if self.game.settings.supercarrier:
unit_type = self.upgrade_to_supercarrier(unit_type, self.control_point.name) 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: class HelipadGenerator:
""" """
Generates helipads for given control point Generates helipads for given control point
@ -726,20 +670,8 @@ class TgoGenerator:
self.helipads[cp] = helipad_gen.helipads self.helipads[cp] = helipad_gen.helipads
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
generator: GenericGroundObjectGenerator[Any] generator: GroundObjectGenerator
if isinstance(ground_object, FactoryGroundObject): if isinstance(ground_object, CarrierGroundObject):
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 = CarrierGenerator( generator = CarrierGenerator(
ground_object, ground_object,
cp, cp,
@ -753,7 +685,7 @@ class TgoGenerator:
self.unit_map, self.unit_map,
) )
elif isinstance(ground_object, LhaGroundObject): elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator( generator = LhaGenerator(
ground_object, ground_object,
cp, cp,
country, country,
@ -765,16 +697,12 @@ class TgoGenerator:
self.runways, self.runways,
self.unit_map, 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): elif isinstance(ground_object, MissileSiteGroundObject):
generator = MissileSiteGenerator( generator = MissileSiteGenerator(
ground_object, country, self.game, self.m, self.unit_map ground_object, country, self.game, self.m, self.unit_map
) )
else: else:
generator = GenericGroundObjectGenerator( generator = GroundObjectGenerator(
ground_object, country, self.game, self.m, self.unit_map ground_object, country, self.game, self.m, self.unit_map
) )
generator.generate() generator.generate()

View File

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

View File

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

View File

@ -29,8 +29,7 @@ class MissionResultsProcessor:
self.commit_convoy_losses(debriefing) self.commit_convoy_losses(debriefing)
self.commit_cargo_ship_losses(debriefing) self.commit_cargo_ship_losses(debriefing)
self.commit_airlift_losses(debriefing) self.commit_airlift_losses(debriefing)
self.commit_ground_object_losses(debriefing) self.commit_ground_losses(debriefing)
self.commit_building_losses(debriefing)
self.commit_damaged_runways(debriefing) self.commit_damaged_runways(debriefing)
self.commit_captures(debriefing) self.commit_captures(debriefing)
self.commit_front_line_battle_impact(debriefing) self.commit_front_line_battle_impact(debriefing)
@ -131,23 +130,11 @@ class MissionResultsProcessor:
) )
@staticmethod @staticmethod
def commit_ground_object_losses(debriefing: Debriefing) -> None: def commit_ground_losses(debriefing: Debriefing) -> None:
for loss in debriefing.ground_object_losses: for ground_object_loss in debriefing.ground_object_losses:
# TODO: This should be stored in the TGO, not in the pydcs Group. ground_object_loss.ground_unit.kill()
if not hasattr(loss.group, "units_losts"): for scenery_object_loss in debriefing.scenery_object_losses:
loss.group.units_losts = [] # type: ignore scenery_object_loss.ground_unit.kill()
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}",
)
@staticmethod @staticmethod
def commit_damaged_runways(debriefing: Debriefing) -> None: 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 .latlon import LatLon
from .projections import TransverseMercator from .projections import TransverseMercator
from .seasonalconditions import SeasonalConditions from .seasonalconditions import SeasonalConditions
from ..utils import Heading
if TYPE_CHECKING: if TYPE_CHECKING:
from .controlpoint import ControlPoint, MissionTarget from .controlpoint import ControlPoint, MissionTarget
@ -85,7 +86,7 @@ class ConflictTheater:
def find_ground_objects_by_obj_name( def find_ground_objects_by_obj_name(
self, obj_name: str self, obj_name: str
) -> list[TheaterGroundObject[Any]]: ) -> list[TheaterGroundObject]:
found = [] found = []
for cp in self.controlpoints: for cp in self.controlpoints:
for g in cp.ground_objects: for g in cp.ground_objects:
@ -265,6 +266,29 @@ class ConflictTheater:
x, y = self.ll_to_point_transformer.transform(ll.lat, ll.lng) x, y = self.ll_to_point_transformer.transform(ll.lat, ll.lng)
return Point(x, y) 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): class CaucasusTheater(ConflictTheater):
terrain = caucasus.Caucasus() terrain = caucasus.Caucasus()

View File

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

View File

@ -4,19 +4,23 @@ import itertools
import logging import logging
from abc import ABC from abc import ABC
from collections.abc import Sequence 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.mapping import Point
from dcs.triggers import TriggerZone from dcs.triggers import TriggerZone
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unitgroup import ShipGroup, VehicleGroup
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
from game.dcs.helpers import unit_type_from_name from game.dcs.helpers import unit_type_from_name
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS 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 from ..utils import Distance, Heading, meters
if TYPE_CHECKING: if TYPE_CHECKING:
from gen.templates import UnitTemplate, GroupTemplate, TemplateRandomizer
from .controlpoint import ControlPoint from .controlpoint import ControlPoint
from ..ato.flighttype import FlightType 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__( def __init__(
self, self,
name: str, name: str,
category: str, category: str,
group_id: int,
position: Point, position: Point,
heading: Heading, heading: Heading,
control_point: ControlPoint, control_point: ControlPoint,
dcs_identifier: str,
sea_object: bool, sea_object: bool,
) -> None: ) -> None:
super().__init__(name, position) super().__init__(name, position)
self.category = category self.category = category
self.group_id = group_id
self.heading = heading self.heading = heading
self.control_point = control_point self.control_point = control_point
self.dcs_identifier = dcs_identifier
self.sea_object = sea_object self.sea_object = sea_object
self.groups: List[GroupT] = [] self.groups: List[GroundGroup] = []
@property @property
def is_dead(self) -> bool: def is_dead(self) -> bool:
return self.alive_unit_count == 0 return self.alive_unit_count == 0
@property @property
def units(self) -> List[Unit]: def units(self) -> Iterator[GroundUnit]:
""" """
:return: all the units at this location :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 @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: all the dead units at this location
""" """
return list( return [unit for unit in self.units if not unit.alive]
itertools.chain.from_iterable(
[getattr(g, "units_losts", []) for g in self.groups]
)
)
@property @property
def group_name(self) -> str: def group_name(self) -> str:
"""The name of the unit group.""" """The name of the unit group."""
return f"{self.category}|{self.group_id}" return f"{self.category}|{self.name}"
@property @property
def waypoint_name(self) -> str: def waypoint_name(self) -> str:
@ -103,9 +238,6 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
def __str__(self) -> str: def __str__(self) -> str:
return NAME_BY_CATEGORY[self.category] return NAME_BY_CATEGORY[self.category]
def is_same_group(self, identifier: str) -> bool:
return self.group_id == identifier
@property @property
def obj_name(self) -> str: def obj_name(self) -> str:
return self.name return self.name
@ -135,7 +267,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
@property @property
def alive_unit_count(self) -> int: 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 @property
def might_have_aa(self) -> bool: def might_have_aa(self) -> bool:
@ -149,7 +281,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
return True return True
return False 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: if not self.might_have_aa:
return meters(0) return meters(0)
@ -170,7 +302,7 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
def max_detection_range(self) -> Distance: def max_detection_range(self) -> Distance:
return max(self.detection_range(g) for g in self.groups) 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") return self._max_range_of_type(group, "detection_range")
def max_threat_range(self) -> Distance: 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) 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") return self._max_range_of_type(group, "threat_range")
@property @property
@ -195,8 +327,8 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
return False return False
@property @property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]: def strike_targets(self) -> list[GroundUnit]:
return self.units return [unit for unit in self.units if unit.alive]
@property @property
def mark_locations(self) -> Iterator[Point]: def mark_locations(self) -> Iterator[Point]:
@ -214,70 +346,31 @@ class TheaterGroundObject(MissionTarget, Generic[GroupT]):
raise NotImplementedError raise NotImplementedError
class BuildingGroundObject(TheaterGroundObject[VehicleGroup]): class BuildingGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
category: str, category: str,
group_id: int,
object_id: int,
position: Point, position: Point,
heading: Heading, heading: Heading,
control_point: ControlPoint, control_point: ControlPoint,
dcs_identifier: str,
is_fob_structure: bool = False, is_fob_structure: bool = False,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
category=category, category=category,
group_id=group_id,
position=position, position=position,
heading=heading, heading=heading,
control_point=control_point, control_point=control_point,
dcs_identifier=dcs_identifier,
sea_object=False, sea_object=False,
) )
self.is_fob_structure = is_fob_structure 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 @property
def mark_locations(self) -> Iterator[Point]: def mark_locations(self) -> Iterator[Point]:
for building in self.iter_building_group(): # Special handling to mark all buildings of the TGO
yield building.position for unit in self.strike_targets:
yield unit.position
@property @property
def is_control_point(self) -> bool: def is_control_point(self) -> bool:
@ -298,55 +391,7 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
return meters(0) return meters(0)
class SceneryGroundObject(BuildingGroundObject): class NavalGroundObject(TheaterGroundObject):
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]):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from game.ato import FlightType from game.ato import FlightType
@ -375,15 +420,13 @@ class GenericCarrierGroundObject(NavalGroundObject):
# TODO: Why is this both a CP and a TGO? # TODO: Why is this both a CP and a TGO?
class CarrierGroundObject(GenericCarrierGroundObject): 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__( super().__init__(
name=name, name=name,
category="CARRIER", category="CARRIER",
group_id=group_id,
position=control_point.position, position=control_point.position,
heading=Heading.from_degrees(0), heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="CARRIER",
sea_object=True, sea_object=True,
) )
@ -399,15 +442,13 @@ class CarrierGroundObject(GenericCarrierGroundObject):
# TODO: Why is this both a CP and a TGO? # TODO: Why is this both a CP and a TGO?
class LhaGroundObject(GenericCarrierGroundObject): 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__( super().__init__(
name=name, name=name,
category="LHA", category="LHA",
group_id=group_id,
position=control_point.position, position=control_point.position,
heading=Heading.from_degrees(0), heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="LHA",
sea_object=True, sea_object=True,
) )
@ -421,18 +462,14 @@ class LhaGroundObject(GenericCarrierGroundObject):
return f"LHA {self.name}" return f"LHA {self.name}"
class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]): class MissileSiteGroundObject(TheaterGroundObject):
def __init__( def __init__(self, name: str, position: Point, control_point: ControlPoint) -> None:
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="missile", category="missile",
group_id=group_id,
position=position, position=position,
heading=Heading.from_degrees(0), heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="AA",
sea_object=False, sea_object=False,
) )
@ -445,11 +482,10 @@ class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
return False return False
class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]): class CoastalSiteGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
group_id: int,
position: Point, position: Point,
control_point: ControlPoint, control_point: ControlPoint,
heading: Heading, heading: Heading,
@ -457,11 +493,9 @@ class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
super().__init__( super().__init__(
name=name, name=name,
category="coastal", category="coastal",
group_id=group_id,
position=position, position=position,
heading=heading, heading=heading,
control_point=control_point, control_point=control_point,
dcs_identifier="AA",
sea_object=False, sea_object=False,
) )
@ -474,7 +508,7 @@ class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
return False return False
class IadsGroundObject(TheaterGroundObject[VehicleGroup], ABC): class IadsGroundObject(TheaterGroundObject, ABC):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from game.ato import FlightType from game.ato import FlightType
@ -490,18 +524,15 @@ class SamGroundObject(IadsGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
group_id: int,
position: Point, position: Point,
control_point: ControlPoint, control_point: ControlPoint,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="aa", category="aa",
group_id=group_id,
position=position, position=position,
heading=Heading.from_degrees(0), heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="AA",
sea_object=False, sea_object=False,
) )
@ -521,7 +552,7 @@ class SamGroundObject(IadsGroundObject):
def might_have_aa(self) -> bool: def might_have_aa(self) -> bool:
return True 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) max_non_radar = meters(0)
live_trs = set() live_trs = set()
max_telar_range = meters(0) max_telar_range = meters(0)
@ -554,22 +585,19 @@ class SamGroundObject(IadsGroundObject):
return True return True
class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]): class VehicleGroupGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
group_id: int,
position: Point, position: Point,
control_point: ControlPoint, control_point: ControlPoint,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="armor", category="armor",
group_id=group_id,
position=position, position=position,
heading=Heading.from_degrees(0), heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="AA",
sea_object=False, sea_object=False,
) )
@ -586,18 +614,15 @@ class EwrGroundObject(IadsGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
group_id: int,
position: Point, position: Point,
control_point: ControlPoint, control_point: ControlPoint,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="ewr", category="ewr",
group_id=group_id,
position=position, position=position,
heading=Heading.from_degrees(0), heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="EWR",
sea_object=False, sea_object=False,
) )
@ -605,7 +630,7 @@ class EwrGroundObject(IadsGroundObject):
def group_name(self) -> str: def group_name(self) -> str:
# Prefix the group names with the side color so Skynet can find them. # Prefix the group names with the side color so Skynet can find them.
# Use Group Id and uppercase EWR # Use Group Id and uppercase EWR
return f"{self.faction_color}|EWR|{self.group_id}" return f"{self.faction_color}|EWR|{self.name}"
@property @property
def might_have_aa(self) -> bool: def might_have_aa(self) -> bool:
@ -621,17 +646,13 @@ class EwrGroundObject(IadsGroundObject):
class ShipGroundObject(NavalGroundObject): class ShipGroundObject(NavalGroundObject):
def __init__( def __init__(self, name: str, position: Point, control_point: ControlPoint) -> None:
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
super().__init__( super().__init__(
name=name, name=name,
category="ship", category="ship",
group_id=group_id,
position=position, position=position,
heading=Heading.from_degrees(0), heading=Heading.from_degrees(0),
control_point=control_point, control_point=control_point,
dcs_identifier="AA",
sea_object=True, sea_object=True,
) )

View File

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

View File

@ -4,16 +4,17 @@ from __future__ import annotations
import itertools import itertools
import math import math
from dataclasses import dataclass 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.triggers import TriggerZone
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup from dcs.unit import Unit
from dcs.unitgroup import FlyingGroup, VehicleGroup, ShipGroup
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.squadrons import Pilot from game.squadrons import Pilot
from game.theater import Airfield, ControlPoint, TheaterGroundObject from game.theater import Airfield, ControlPoint, GroundUnit
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
from game.ato.flight import Flight from game.ato.flight import Flight
from game.theater.theatergroundobject import SceneryGroundUnit
if TYPE_CHECKING: if TYPE_CHECKING:
from game.transfers import CargoShip, Convoy, TransferOrder from game.transfers import CargoShip, Convoy, TransferOrder
@ -31,14 +32,16 @@ class FrontLineUnit:
origin: ControlPoint origin: ControlPoint
UnitT = TypeVar("UnitT", Ship, Vehicle) @dataclass(frozen=True)
class GroundObjectMapping:
ground_unit: GroundUnit
dcs_unit: Unit
@dataclass(frozen=True) @dataclass(frozen=True)
class GroundObjectUnit(Generic[UnitT]): class SceneryObjectMapping:
ground_object: TheaterGroundObject[Any] ground_unit: GroundUnit
group: MovingGroup[UnitT] trigger_zone: TriggerZone
unit: UnitT
@dataclass(frozen=True) @dataclass(frozen=True)
@ -53,18 +56,13 @@ class AirliftUnits:
transfer: TransferOrder transfer: TransferOrder
@dataclass(frozen=True)
class Building:
ground_object: BuildingGroundObject
class UnitMap: class UnitMap:
def __init__(self) -> None: def __init__(self) -> None:
self.aircraft: Dict[str, FlyingUnit] = {} self.aircraft: Dict[str, FlyingUnit] = {}
self.airfields: Dict[str, Airfield] = {} self.airfields: Dict[str, Airfield] = {}
self.front_line_units: Dict[str, FrontLineUnit] = {} self.front_line_units: Dict[str, FrontLineUnit] = {}
self.ground_object_units: Dict[str, GroundObjectUnit[Any]] = {} self.ground_objects: Dict[str, GroundObjectMapping] = {}
self.buildings: Dict[str, Building] = {} self.scenery_objects: Dict[str, SceneryObjectMapping] = {}
self.convoys: Dict[str, ConvoyUnit] = {} self.convoys: Dict[str, ConvoyUnit] = {}
self.cargo_ships: Dict[str, CargoShip] = {} self.cargo_ships: Dict[str, CargoShip] = {}
self.airlifts: Dict[str, AirliftUnits] = {} self.airlifts: Dict[str, AirliftUnits] = {}
@ -105,41 +103,18 @@ class UnitMap:
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]: def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
return self.front_line_units.get(name, None) return self.front_line_units.get(name, None)
def add_ground_object_units( def add_ground_object_mapping(
self, self, ground_unit: GroundUnit, dcs_unit: Unit
ground_object: TheaterGroundObject[Any],
persistence_group: Union[ShipGroup, VehicleGroup],
miz_group: Union[ShipGroup, VehicleGroup],
) -> None: ) -> 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: def ground_object(self, name: str) -> Optional[GroundObjectMapping]:
ground_object: The TGO the group is associated with. return self.ground_objects.get(name, None)
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 add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None: def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None:
for unit, unit_type in zip(group.units, convoy.iter_units()): 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]: def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
return self.airlifts.get(name, None) return self.airlifts.get(name, None)
def add_building( def add_scenery(
self, ground_object: BuildingGroundObject, group: StaticGroup self, scenery_unit: SceneryGroundUnit, trigger_zone: TriggerZone
) -> None: ) -> None:
# The actual name is a String (the pydcs translatable string), which name = str(trigger_zone.name)
# doesn't define __eq__. if name in self.scenery_objects:
# The name of the initiator in the DCS dead event will have " object" raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)")
# appended for statics. self.scenery_objects[name] = SceneryObjectMapping(scenery_unit, trigger_zone)
name = f"{group.name} object"
if name in self.buildings:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object)
def add_fortification( def scenery_object(self, name: str) -> Optional[SceneryObjectMapping]:
self, ground_object: BuildingGroundObject, group: VehicleGroup return self.scenery_objects.get(name, None)
) -> 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)

View File

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

View File

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

View File

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

View File

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

View File

@ -3,11 +3,8 @@ from __future__ import annotations
from typing import List, Optional from typing import List, Optional
from PySide2.QtCore import Property, QObject, Signal, Slot 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 import Game
from game.dcs.groundunittype import GroundUnitType
from game.server.leaflet import LeafletLatLon from game.server.leaflet import LeafletLatLon
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
@ -30,7 +27,6 @@ class GroundObjectJs(QObject):
self.tgo = tgo self.tgo = tgo
self.game = game self.game = game
self.theater = game.theater self.theater = game.theater
self.buildings = self.theater.find_ground_objects_by_obj_name(self.tgo.obj_name)
self.dialog: Optional[QGroundObjectMenu] = None self.dialog: Optional[QGroundObjectMenu] = None
@Slot() @Slot()
@ -39,7 +35,6 @@ class GroundObjectJs(QObject):
self.dialog = QGroundObjectMenu( self.dialog = QGroundObjectMenu(
None, None,
self.tgo, self.tgo,
self.buildings,
self.tgo.control_point, self.tgo.control_point,
self.game, self.game,
) )
@ -61,38 +56,9 @@ class GroundObjectJs(QObject):
def category(self) -> str: def category(self) -> str:
return self.tgo.category 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) @Property(list, notify=unitsChanged)
def units(self) -> List[str]: def units(self) -> List[str]:
units = [] return [unit.display_name for unit in self.tgo.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
@Property(bool, notify=blueChanged) @Property(bool, notify=blueChanged)
def blue(self) -> bool: def blue(self) -> bool:
@ -105,9 +71,7 @@ class GroundObjectJs(QObject):
@Property(bool, notify=deadChanged) @Property(bool, notify=deadChanged)
def dead(self) -> bool: def dead(self) -> bool:
if not self.tgo.groups: return not any(g.alive_units > 0 for g in self.tgo.groups)
return all(b.is_dead for b in self.buildings)
return not any(g.units for g in self.tgo.groups)
@Property(list, notify=samThreatRangesChanged) @Property(list, notify=samThreatRangesChanged)
def samThreatRanges(self) -> List[float]: def samThreatRanges(self) -> List[float]:

View File

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

View File

@ -169,12 +169,14 @@ class QWaitingForMissionResultWindow(QDialog):
update_layout, update_layout,
) )
self.add_update_row( self.add_update_row(
"Ground units lost at objective areas", "Ground Objects destroyed",
len(list(debriefing.ground_object_losses)), len(list(debriefing.ground_object_losses)),
update_layout, update_layout,
) )
self.add_update_row( 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( self.add_update_row(
"Base capture events", len(debriefing.base_captures), update_layout "Base capture events", len(debriefing.base_captures), update_layout

View File

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

View File

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