diff --git a/game/data/units.py b/game/data/units.py index 5d6bf2cd..a03b64d7 100644 --- a/game/data/units.py +++ b/game/data/units.py @@ -40,3 +40,24 @@ class UnitClass(Enum): TANK = "Tank" TELAR = "TELAR" TRACK_RADAR = "TrackRadar" + + +# All UnitClasses which can have AntiAir capabilities +ANTI_AIR_UNIT_CLASSES = [ + UnitClass.AAA, + UnitClass.AIRCRAFT_CARRIER, + UnitClass.CRUISER, + UnitClass.DESTROYER, + UnitClass.EARLY_WARNING_RADAR, + UnitClass.FRIGATE, + UnitClass.HELICOPTER_CARRIER, + UnitClass.LAUNCHER, + UnitClass.MANPAD, + UnitClass.SEARCH_RADAR, + UnitClass.SEARCH_TRACK_RADAR, + UnitClass.SPECIALIZED_RADAR, + UnitClass.SHORAD, + UnitClass.SUBMARINE, + UnitClass.TELAR, + UnitClass.TRACK_RADAR, +] diff --git a/game/missiongenerator/luagenerator.py b/game/missiongenerator/luagenerator.py index 2eb7f324..9cfc7ac5 100644 --- a/game/missiongenerator/luagenerator.py +++ b/game/missiongenerator/luagenerator.py @@ -118,22 +118,17 @@ class LuaGenerator: else lua_data.get_or_create_item("RedAA") ) for ground_object in cp.ground_objects: - if ground_object.might_have_aa and not ground_object.is_dead: - for g in ground_object.groups: - threat_range = ground_object.threat_range(g) + for g in ground_object.groups: + threat_range = g.max_threat_range() - if not threat_range: - continue + if not threat_range: + continue - aa_item = coalition_object.add_item() - aa_item.add_key_value("name", ground_object.name) - aa_item.add_key_value("range", str(threat_range.meters)) - aa_item.add_key_value( - "positionX", str(ground_object.position.x) - ) - aa_item.add_key_value( - "positionY", str(ground_object.position.y) - ) + aa_item = coalition_object.add_item() + aa_item.add_key_value("name", ground_object.name) + aa_item.add_key_value("range", str(threat_range.meters)) + aa_item.add_key_value("positionX", str(ground_object.position.x)) + aa_item.add_key_value("positionY", str(ground_object.position.y)) # Generate IADS Lua Item iads_object = lua_data.add_item("IADS") diff --git a/game/server/tgos/models.py b/game/server/tgos/models.py index 1133fd39..7124f050 100644 --- a/game/server/tgos/models.py +++ b/game/server/tgos/models.py @@ -30,14 +30,8 @@ class TgoJs(BaseModel): @staticmethod def for_tgo(tgo: TheaterGroundObject) -> TgoJs: - if not tgo.might_have_aa: - threat_ranges = [] - detection_ranges = [] - else: - threat_ranges = [tgo.threat_range(group).meters for group in tgo.groups] - detection_ranges = [ - tgo.detection_range(group).meters for group in tgo.groups - ] + threat_ranges = [group.max_threat_range().meters for group in tgo.groups] + detection_ranges = [group.max_detection_range().meters for group in tgo.groups] return TgoJs( id=tgo.id, name=tgo.name, diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index d622a9bf..d1545f0f 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -3,13 +3,10 @@ from __future__ import annotations import itertools import uuid from abc import ABC -from typing import Type from typing import Any, Iterator, List, Optional, TYPE_CHECKING from dcs.mapping import Point -from dcs.unittype import VehicleType -from dcs.unittype import ShipType from shapely.geometry import Point as ShapelyPoint from game.sidc import ( @@ -25,7 +22,6 @@ from game.sidc import ( ) from game.theater.presetlocation import PresetLocation from .missiontarget import MissionTarget -from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS from ..utils import Distance, Heading, meters if TYPE_CHECKING: @@ -170,54 +166,29 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC): @property def unit_count(self) -> int: - return sum([g.unit_count for g in self.groups]) + return sum(g.unit_count for g in self.groups) @property def alive_unit_count(self) -> int: - return sum([g.alive_units for g in self.groups]) + return sum(g.alive_units for g in self.groups) @property - def might_have_aa(self) -> bool: - return False + def has_aa(self) -> bool: + """Returns True if the ground object contains a working anti air unit""" + return any(u.alive and u.is_anti_air for u in self.units) @property def has_live_radar_sam(self) -> bool: """Returns True if the ground object contains a unit with working radar SAM.""" - for group in self.groups: - if self.threat_range(group, radar_only=True): - return True - return False - - def _max_range_of_type(self, group: TheaterGroup, range_type: str) -> Distance: - if not self.might_have_aa: - return meters(0) - - max_range = meters(0) - for u in group.units: - # Some units in pydcs have detection_range/threat_range defined, - # but explicitly set to None. - unit_range = getattr(u.type, range_type, None) - if unit_range is not None: - max_range = max(max_range, meters(unit_range)) - return max_range + return any(g.max_threat_range(radar_only=True) for g in self.groups) def max_detection_range(self) -> Distance: - return ( - max(self.detection_range(g) for g in self.groups) - if self.groups - else meters(0) - ) - - def detection_range(self, group: TheaterGroup) -> Distance: - return self._max_range_of_type(group, "detection_range") + """Calculate the maximum detection range of the ground object""" + return max((g.max_detection_range() for g in self.groups), default=meters(0)) def max_threat_range(self) -> Distance: - return ( - max(self.threat_range(g) for g in self.groups) if self.groups else meters(0) - ) - - def threat_range(self, group: TheaterGroup, radar_only: bool = False) -> Distance: - return self._max_range_of_type(group, "threat_range") + """Calculate the maximum threat range of the ground object""" + return max((g.max_threat_range() for g in self.groups), default=meters(0)) def threat_poly(self) -> ThreatPoly | None: if self._threat_poly is None: @@ -360,12 +331,6 @@ class BuildingGroundObject(TheaterGroundObject): def purchasable(self) -> bool: return False - def max_threat_range(self) -> Distance: - return meters(0) - - def max_detection_range(self) -> Distance: - return meters(0) - class NavalGroundObject(TheaterGroundObject, ABC): def mission_types(self, for_player: bool) -> Iterator[FlightType]: @@ -375,10 +340,6 @@ class NavalGroundObject(TheaterGroundObject, ABC): yield FlightType.ANTISHIP yield from super().mission_types(for_player) - @property - def might_have_aa(self) -> bool: - return True - @property def capturable(self) -> bool: return False @@ -554,36 +515,6 @@ class SamGroundObject(IadsGroundObject): if mission_type is not FlightType.DEAD: yield mission_type - @property - def might_have_aa(self) -> bool: - return True - - def threat_range(self, group: TheaterGroup, radar_only: bool = False) -> Distance: - max_non_radar = meters(0) - live_trs = set() - max_telar_range = meters(0) - launchers = set() - for unit in group.units: - if not unit.alive or not issubclass(unit.type, VehicleType): - continue - unit_type = unit.type - if unit_type in TRACK_RADARS: - live_trs.add(unit_type) - elif unit_type in TELARS: - max_telar_range = max(max_telar_range, meters(unit_type.threat_range)) - elif unit_type in LAUNCHER_TRACKER_PAIRS: - launchers.add(unit_type) - else: - max_non_radar = max(max_non_radar, meters(unit_type.threat_range)) - max_tel_range = meters(0) - for launcher in launchers: - if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs: - max_tel_range = max(max_tel_range, meters(launcher.threat_range)) - if radar_only: - return max(max_tel_range, max_telar_range) - else: - return max(max_tel_range, max_telar_range, max_non_radar) - @property def capturable(self) -> bool: return False @@ -642,10 +573,6 @@ class EwrGroundObject(IadsGroundObject): def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: return SymbolSet.LAND_EQUIPMENT, LandEquipmentEntity.RADAR - @property - def might_have_aa(self) -> bool: - return True - @property def capturable(self) -> bool: return False diff --git a/game/theater/theatergroup.py b/game/theater/theatergroup.py index cc709fe3..6a23d8de 100644 --- a/game/theater/theatergroup.py +++ b/game/theater/theatergroup.py @@ -2,23 +2,23 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any, Optional, TYPE_CHECKING, Type -from enum import Enum from dcs.triggers import TriggerZone from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType -from game.data.groups import GroupTask +from game.data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS +from game.data.units import ANTI_AIR_UNIT_CLASSES from game.dcs.groundunittype import GroundUnitType from game.dcs.shipunittype import ShipUnitType from game.dcs.unittype import UnitType from game.point_with_heading import PointWithHeading from game.theater.iadsnetwork.iadsrole import IadsRole -from game.utils import Heading, Distance +from game.utils import Heading, Distance, meters if TYPE_CHECKING: from game.layout.layout import LayoutUnit from game.sim import GameUpdateEvents - from game.theater import TheaterGroundObject + from game.theater.theatergroundobject import TheaterGroundObject @dataclass @@ -91,6 +91,13 @@ class TheaterUnit: def is_ship(self) -> bool: return issubclass(self.type, ShipType) + @property + def is_anti_air(self) -> bool: + return ( + self.unit_type is not None + and self.unit_type.unit_class in ANTI_AIR_UNIT_CLASSES + ) + @property def icon(self) -> str: return self.type.id @@ -100,6 +107,16 @@ class TheaterUnit: # Only let units with UnitType be repairable as we just have prices for them return self.unit_type is not None + @property + def detection_range(self) -> Distance: + unit_range = getattr(self.type, "detection_range", None) + return meters(unit_range if unit_range is not None and self.alive else 0) + + @property + def threat_range(self) -> Distance: + unit_range = getattr(self.type, "threat_range", None) + return meters(unit_range if unit_range is not None and self.alive else 0) + class SceneryUnit(TheaterUnit): """Special TheaterUnit for handling scenery ground objects""" @@ -166,7 +183,41 @@ class TheaterGroup: @property def alive_units(self) -> int: - return sum([unit.alive for unit in self.units]) + return sum(unit.alive for unit in self.units) + + def max_detection_range(self) -> Distance: + """Calculate the maximum detection range of the TheaterGroup""" + ranges = (u.detection_range for u in self.units if u.is_anti_air) + return max(ranges, default=meters(0)) + + def max_threat_range(self, radar_only: bool = False) -> Distance: + """Calculate the maximum threat range of the TheaterGroup. + This also checks for Launcher and Tracker Pairs and if they are functioning or not. Allows to also use only radar emitting units for the calculation with the parameter.""" + max_non_radar = meters(0) + max_telar_range = meters(0) + max_tel_range = meters(0) + live_trs = set() + launchers: dict[Type[VehicleType], Distance] = {} + for unit in self.units: + if not unit.alive or not unit.is_anti_air: + continue + if unit.type in TRACK_RADARS: + live_trs.add(unit.type) + elif unit.type in TELARS: + max_telar_range = max(max_telar_range, unit.threat_range) + elif ( + issubclass(unit.type, VehicleType) + and unit.type in LAUNCHER_TRACKER_PAIRS + ): + launchers[unit.type] = unit.threat_range + else: + max_non_radar = max(max_non_radar, unit.threat_range) + for launcher, threat_range in launchers.items(): + if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs: + max_tel_range = max(max_tel_range, threat_range) + if radar_only: + return max(max_tel_range, max_telar_range) + return max(max_tel_range, max_telar_range, max_non_radar) class IadsGroundGroup(TheaterGroup): diff --git a/game/threatzones.py b/game/threatzones.py index ec52a8c3..07fec2bd 100644 --- a/game/threatzones.py +++ b/game/threatzones.py @@ -202,9 +202,9 @@ class ThreatZones: """ air_threats = [] air_defenses = [] - for control_point in game.theater.control_points_for(player): - air_threats.append(control_point) - air_defenses.extend(control_point.ground_objects) + for cp in game.theater.control_points_for(player): + air_threats.append(cp) + air_defenses.extend([go for go in cp.ground_objects if go.has_aa]) return cls.for_threats( game.theater, game.faction_for(player).doctrine, air_threats, air_defenses @@ -241,17 +241,17 @@ class ThreatZones: for tgo in air_defenses: for group in tgo.groups: - threat_range = tgo.threat_range(group) + threat_range = group.max_threat_range() # Any system with a shorter range than this is not worth # even avoiding. if threat_range > nautical_miles(3): point = ShapelyPoint(tgo.position.x, tgo.position.y) threat_zone = point.buffer(threat_range.meters) air_defense_threats.append(threat_zone) - radar_threat_range = tgo.threat_range(group, radar_only=True) + radar_threat_range = group.max_threat_range(radar_only=True) if radar_threat_range > nautical_miles(3): point = ShapelyPoint(tgo.position.x, tgo.position.y) - threat_zone = point.buffer(threat_range.meters) + threat_zone = point.buffer(radar_threat_range.meters) radar_sam_threats.append(threat_zone) return ThreatZones(