mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
The `FactoryGroundObject` is just a special case of `BuildingGroundObject` that we maybe don't actually need. For now it provides some special case logic for the layout, but this allows any TGO with the "factory" category to behave as a ground unit source. Note that the "factory" random strike targets are *not* generated anymore, so this doesn't affect campaign design currently.
528 lines
15 KiB
Python
528 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
import itertools
|
|
import logging
|
|
from typing import Iterator, List, TYPE_CHECKING
|
|
|
|
from dcs.mapping import Point
|
|
from dcs.unit import Unit
|
|
from dcs.unitgroup import Group
|
|
|
|
from .. import db
|
|
from ..data.radar_db import UNITS_WITH_RADAR
|
|
from ..utils import Distance, meters
|
|
|
|
if TYPE_CHECKING:
|
|
from .controlpoint import ControlPoint
|
|
from gen.flights.flight import FlightType
|
|
|
|
from .missiontarget import MissionTarget
|
|
|
|
NAME_BY_CATEGORY = {
|
|
"power": "Power plant",
|
|
"ammo": "Ammo depot",
|
|
"fuel": "Fuel depot",
|
|
"aa": "AA Defense Site",
|
|
"ware": "Warehouse",
|
|
"farp": "FARP",
|
|
"fob": "FOB",
|
|
"factory": "Factory",
|
|
"comms": "Comms. tower",
|
|
"oil": "Oil platform",
|
|
"derrick": "Derrick",
|
|
"ww2bunker": "Bunker",
|
|
"village": "Village",
|
|
"allycamp": "Camp",
|
|
"EWR": "EWR",
|
|
}
|
|
|
|
ABBREV_NAME = {
|
|
"power": "PLANT",
|
|
"ammo": "AMMO",
|
|
"fuel": "FUEL",
|
|
"aa": "AA",
|
|
"ware": "WARE",
|
|
"farp": "FARP",
|
|
"fob": "FOB",
|
|
"factory": "FACTORY",
|
|
"comms": "COMMST",
|
|
"oil": "OILP",
|
|
"derrick": "DERK",
|
|
"ww2bunker": "BUNK",
|
|
"village": "VLG",
|
|
"allycamp": "CMP",
|
|
}
|
|
|
|
CATEGORY_MAP = {
|
|
# Special cases
|
|
"CARRIER": ["CARRIER"],
|
|
"LHA": ["LHA"],
|
|
"aa": ["AA"],
|
|
# Buildings
|
|
"power": [
|
|
"Workshop A",
|
|
"Electric power box",
|
|
"Garage small A",
|
|
"Farm B",
|
|
"Repair workshop",
|
|
"Garage B",
|
|
],
|
|
"ware": ["Warehouse", "Hangar A"],
|
|
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
|
|
"ammo": [".Ammunition depot", "Hangar B"],
|
|
"farp": [
|
|
"FARP Tent",
|
|
"FARP Ammo Dump Coating",
|
|
"FARP Fuel Depot",
|
|
"FARP Command Post",
|
|
"FARP CP Blindage",
|
|
],
|
|
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
|
|
"factory": ["Tech combine", "Tech hangar A"],
|
|
"comms": ["TV tower", "Comms tower M"],
|
|
"oil": ["Oil platform"],
|
|
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
|
|
"ww2bunker": [
|
|
"Siegfried Line",
|
|
"Fire Control Bunker",
|
|
"SK_C_28_naval_gun",
|
|
"Concertina Wire",
|
|
"Czech hedgehogs 1",
|
|
],
|
|
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
|
|
"allycamp": [],
|
|
}
|
|
|
|
|
|
class TheaterGroundObject(MissionTarget):
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
category: str,
|
|
group_id: int,
|
|
position: Point,
|
|
heading: int,
|
|
control_point: ControlPoint,
|
|
dcs_identifier: str,
|
|
airbase_group: bool,
|
|
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.airbase_group = airbase_group
|
|
self.sea_object = sea_object
|
|
self.groups: List[Group] = []
|
|
|
|
@property
|
|
def is_dead(self) -> bool:
|
|
return self.alive_unit_count == 0
|
|
|
|
@property
|
|
def units(self) -> List[Unit]:
|
|
"""
|
|
:return: all the units at this location
|
|
"""
|
|
return list(itertools.chain.from_iterable([g.units for g in self.groups]))
|
|
|
|
@property
|
|
def group_name(self) -> str:
|
|
"""The name of the unit group."""
|
|
return f"{self.category}|{self.group_id}"
|
|
|
|
@property
|
|
def waypoint_name(self) -> str:
|
|
return f"[{self.name}] {self.category}"
|
|
|
|
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
|
|
|
|
@property
|
|
def faction_color(self) -> str:
|
|
return "BLUE" if self.control_point.captured else "RED"
|
|
|
|
def is_friendly(self, to_player: bool) -> bool:
|
|
return self.control_point.is_friendly(to_player)
|
|
|
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
from gen.flights.flight import FlightType
|
|
|
|
if self.is_friendly(for_player):
|
|
yield from [
|
|
# TODO: FlightType.LOGISTICS
|
|
# TODO: FlightType.TROOP_TRANSPORT
|
|
]
|
|
else:
|
|
yield from [
|
|
FlightType.STRIKE,
|
|
FlightType.BAI,
|
|
]
|
|
yield from super().mission_types(for_player)
|
|
|
|
@property
|
|
def alive_unit_count(self) -> int:
|
|
return sum(len(g.units) for g in self.groups)
|
|
|
|
@property
|
|
def might_have_aa(self) -> bool:
|
|
return False
|
|
|
|
@property
|
|
def has_radar(self) -> bool:
|
|
"""Returns True if the ground object contains a unit with radar."""
|
|
for group in self.groups:
|
|
for unit in group.units:
|
|
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
|
|
return True
|
|
return False
|
|
|
|
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
|
|
if not self.might_have_aa:
|
|
return meters(0)
|
|
|
|
max_range = meters(0)
|
|
for u in group.units:
|
|
unit = db.unit_type_from_name(u.type)
|
|
if unit is None:
|
|
logging.error(f"Unknown unit type {u.type}")
|
|
continue
|
|
|
|
# Some units in pydcs have detection_range/threat_range defined,
|
|
# but explicitly set to None.
|
|
unit_range = getattr(unit, range_type, None)
|
|
if unit_range is not None:
|
|
max_range = max(max_range, meters(unit_range))
|
|
return max_range
|
|
|
|
def detection_range(self, group: Group) -> Distance:
|
|
return self._max_range_of_type(group, "detection_range")
|
|
|
|
def threat_range(self, group: Group) -> Distance:
|
|
if not self.detection_range(group):
|
|
# For simple SAMs like shilkas, the unit has both a threat and
|
|
# detection range. For complex sites like SA-2s, the launcher has a
|
|
# threat range and the search/track radars have detection ranges. If
|
|
# the site has no detection range it has no radars and can't fire,
|
|
# so it's not actually a threat even if it still has launchers.
|
|
return meters(0)
|
|
return self._max_range_of_type(group, "threat_range")
|
|
|
|
@property
|
|
def is_factory(self) -> bool:
|
|
return self.category == "factory"
|
|
|
|
|
|
class BuildingGroundObject(TheaterGroundObject):
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
category: str,
|
|
group_id: int,
|
|
object_id: int,
|
|
position: Point,
|
|
heading: int,
|
|
control_point: ControlPoint,
|
|
dcs_identifier: str,
|
|
airbase_group=False,
|
|
) -> None:
|
|
super().__init__(
|
|
name=name,
|
|
category=category,
|
|
group_id=group_id,
|
|
position=position,
|
|
heading=heading,
|
|
control_point=control_point,
|
|
dcs_identifier=dcs_identifier,
|
|
airbase_group=airbase_group,
|
|
sea_object=False,
|
|
)
|
|
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
|
|
|
|
|
|
class FactoryGroundObject(BuildingGroundObject):
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
group_id: int,
|
|
position: Point,
|
|
heading: int,
|
|
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",
|
|
airbase_group=False,
|
|
)
|
|
|
|
|
|
class NavalGroundObject(TheaterGroundObject):
|
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
from gen.flights.flight import FlightType
|
|
|
|
if not self.is_friendly(for_player):
|
|
yield FlightType.ANTISHIP
|
|
yield from super().mission_types(for_player)
|
|
|
|
@property
|
|
def might_have_aa(self) -> bool:
|
|
return True
|
|
|
|
|
|
class GenericCarrierGroundObject(NavalGroundObject):
|
|
pass
|
|
|
|
|
|
# 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:
|
|
super().__init__(
|
|
name=name,
|
|
category="CARRIER",
|
|
group_id=group_id,
|
|
position=control_point.position,
|
|
heading=0,
|
|
control_point=control_point,
|
|
dcs_identifier="CARRIER",
|
|
airbase_group=True,
|
|
sea_object=True,
|
|
)
|
|
|
|
@property
|
|
def group_name(self) -> str:
|
|
# Prefix the group names with the side color so Skynet can find them,
|
|
# add to EWR.
|
|
return f"{self.faction_color}|EWR|{super().group_name}"
|
|
|
|
|
|
# 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:
|
|
super().__init__(
|
|
name=name,
|
|
category="LHA",
|
|
group_id=group_id,
|
|
position=control_point.position,
|
|
heading=0,
|
|
control_point=control_point,
|
|
dcs_identifier="LHA",
|
|
airbase_group=True,
|
|
sea_object=True,
|
|
)
|
|
|
|
@property
|
|
def group_name(self) -> str:
|
|
# Prefix the group names with the side color so Skynet can find them,
|
|
# add to EWR.
|
|
return f"{self.faction_color}|EWR|{super().group_name}"
|
|
|
|
|
|
class MissileSiteGroundObject(TheaterGroundObject):
|
|
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=0,
|
|
control_point=control_point,
|
|
dcs_identifier="AA",
|
|
airbase_group=False,
|
|
sea_object=False,
|
|
)
|
|
|
|
|
|
class CoastalSiteGroundObject(TheaterGroundObject):
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
group_id: int,
|
|
position: Point,
|
|
control_point: ControlPoint,
|
|
heading,
|
|
) -> None:
|
|
super().__init__(
|
|
name=name,
|
|
category="aa",
|
|
group_id=group_id,
|
|
position=position,
|
|
heading=heading,
|
|
control_point=control_point,
|
|
dcs_identifier="AA",
|
|
airbase_group=False,
|
|
sea_object=False,
|
|
)
|
|
|
|
|
|
class BaseDefenseGroundObject(TheaterGroundObject):
|
|
"""Base type for all base defenses."""
|
|
|
|
|
|
# TODO: Differentiate types.
|
|
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
|
# be split into their own types.
|
|
class SamGroundObject(BaseDefenseGroundObject):
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
group_id: int,
|
|
position: Point,
|
|
control_point: ControlPoint,
|
|
for_airbase: bool,
|
|
) -> None:
|
|
super().__init__(
|
|
name=name,
|
|
category="aa",
|
|
group_id=group_id,
|
|
position=position,
|
|
heading=0,
|
|
control_point=control_point,
|
|
dcs_identifier="AA",
|
|
airbase_group=for_airbase,
|
|
sea_object=False,
|
|
)
|
|
# Set by the SAM unit generator if the generated group is compatible
|
|
# with Skynet.
|
|
self.skynet_capable = False
|
|
|
|
@property
|
|
def group_name(self) -> str:
|
|
if self.skynet_capable:
|
|
# Prefix the group names of SAM sites with the side color so Skynet
|
|
# can find them.
|
|
return f"{self.faction_color}|SAM|{self.group_id}"
|
|
else:
|
|
return super().group_name
|
|
|
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
from gen.flights.flight import FlightType
|
|
|
|
if not self.is_friendly(for_player):
|
|
yield FlightType.DEAD
|
|
yield from super().mission_types(for_player)
|
|
|
|
@property
|
|
def might_have_aa(self) -> bool:
|
|
return True
|
|
|
|
|
|
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
group_id: int,
|
|
position: Point,
|
|
control_point: ControlPoint,
|
|
for_airbase: bool,
|
|
) -> None:
|
|
super().__init__(
|
|
name=name,
|
|
category="aa",
|
|
group_id=group_id,
|
|
position=position,
|
|
heading=0,
|
|
control_point=control_point,
|
|
dcs_identifier="AA",
|
|
airbase_group=for_airbase,
|
|
sea_object=False,
|
|
)
|
|
|
|
|
|
class EwrGroundObject(BaseDefenseGroundObject):
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
group_id: int,
|
|
position: Point,
|
|
control_point: ControlPoint,
|
|
for_airbase: bool,
|
|
) -> None:
|
|
super().__init__(
|
|
name=name,
|
|
category="EWR",
|
|
group_id=group_id,
|
|
position=position,
|
|
heading=0,
|
|
control_point=control_point,
|
|
dcs_identifier="EWR",
|
|
airbase_group=for_airbase,
|
|
sea_object=False,
|
|
)
|
|
|
|
@property
|
|
def group_name(self) -> str:
|
|
# Prefix the group names with the side color so Skynet can find them.
|
|
return f"{self.faction_color}|{super().group_name}"
|
|
|
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
from gen.flights.flight import FlightType
|
|
|
|
if not self.is_friendly(for_player):
|
|
yield FlightType.DEAD
|
|
yield from super().mission_types(for_player)
|
|
|
|
@property
|
|
def might_have_aa(self) -> bool:
|
|
return True
|
|
|
|
|
|
class ShipGroundObject(NavalGroundObject):
|
|
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=0,
|
|
control_point=control_point,
|
|
dcs_identifier="AA",
|
|
airbase_group=False,
|
|
sea_object=True,
|
|
)
|
|
|
|
@property
|
|
def group_name(self) -> str:
|
|
# Prefix the group names with the side color so Skynet can find them,
|
|
# add to EWR.
|
|
return f"{self.faction_color}|EWR|{super().group_name}"
|