From ddd6e7d18f22de5471d5b9beb322fa81b9dea9bc Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 23 May 2021 11:28:43 -0700 Subject: [PATCH] Improve detection of functional radar SAMs. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1109 --- game/data/radar_db.py | 35 +++++++++++++++++++-- game/theater/theatergroundobject.py | 49 +++++++++++++++++++++++------ game/threatzones.py | 30 ++++++++++++++++-- gen/flights/ai_flight_planner.py | 2 +- qt_ui/widgets/map/mapmodel.py | 10 +++++- resources/ui/map/map.js | 16 ++++++++++ 6 files changed, 126 insertions(+), 16 deletions(-) diff --git a/game/data/radar_db.py b/game/data/radar_db.py index 22891b8a..9bd384af 100644 --- a/game/data/radar_db.py +++ b/game/data/radar_db.py @@ -22,7 +22,38 @@ from dcs.ships import ( ) from dcs.vehicles import AirDefence -UNITS_WITH_RADAR = [ +TELARS = { + AirDefence.SAM_SA_19_Tunguska_Grison, + AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL, + AirDefence.SAM_SA_8_Osa_Gecko_TEL, + AirDefence.SAM_SA_15_Tor_Gauntlet, + AirDefence.SAM_Roland_ADS, +} + +TRACK_RADARS = { + AirDefence.SAM_SA_6_Kub_Straight_Flush_STR, + AirDefence.SAM_SA_3_S_125_Low_Blow_TR, + AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR, + AirDefence.SAM_Hawk_TR__AN_MPQ_46, + AirDefence.SAM_Patriot_STR, + AirDefence.SAM_SA_2_S_75_Fan_Song_TR, + AirDefence.SAM_Rapier_Blindfire_TR, + AirDefence.HQ_7_Self_Propelled_STR, +} + +LAUNCHER_TRACKER_PAIRS = { + AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Straight_Flush_STR, + AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Low_Blow_TR, + AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR, + AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR, + AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_TR__AN_MPQ_46, + AirDefence.SAM_Patriot_LN: AirDefence.SAM_Patriot_STR, + AirDefence.SAM_SA_2_S_75_Guideline_LN: AirDefence.SAM_SA_2_S_75_Fan_Song_TR, + AirDefence.SAM_Rapier_LN: AirDefence.SAM_Rapier_Blindfire_TR, + AirDefence.HQ_7_Self_Propelled_LN: AirDefence.HQ_7_Self_Propelled_STR, +} + +UNITS_WITH_RADAR = { # Radars AirDefence.SAM_SA_19_Tunguska_Grison, AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL, @@ -74,4 +105,4 @@ UNITS_WITH_RADAR = [ Type_052B_Destroyer, Type_054A_Frigate, Type_052C_Destroyer, -] +} diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 7675b256..00e3953d 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -8,9 +8,15 @@ from dcs.mapping import Point from dcs.triggers import TriggerZone from dcs.unit import Unit from dcs.unitgroup import Group +from dcs.unittype import VehicleType from .. import db -from ..data.radar_db import UNITS_WITH_RADAR +from ..data.radar_db import ( + UNITS_WITH_RADAR, + TRACK_RADARS, + TELARS, + LAUNCHER_TRACKER_PAIRS, +) from ..utils import Distance, meters if TYPE_CHECKING: @@ -166,14 +172,7 @@ class TheaterGroundObject(MissionTarget): 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) + def threat_range(self, group: Group, radar_only: bool = False) -> Distance: return self._max_range_of_type(group, "threat_range") @property @@ -459,6 +458,38 @@ class SamGroundObject(BaseDefenseGroundObject): def might_have_aa(self) -> bool: return True + def threat_range(self, group: Group, 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: + unit_type = db.unit_type_from_name(unit.type) + if unit_type is None or not issubclass(unit_type, VehicleType): + continue + if unit_type in TRACK_RADARS: + live_trs.add(unit_type) + elif unit_type in TELARS: + max_telar_range = max( + max_telar_range, meters(getattr(unit_type, "threat_range", 0)) + ) + elif unit_type in LAUNCHER_TRACKER_PAIRS: + launchers.add(unit_type) + else: + max_non_radar = max( + max_non_radar, meters(getattr(unit_type, "threat_range", 0)) + ) + 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(getattr(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) + class VehicleGroupGroundObject(BaseDefenseGroundObject): def __init__( diff --git a/game/threatzones.py b/game/threatzones.py index e4ad0c39..571e7082 100644 --- a/game/threatzones.py +++ b/game/threatzones.py @@ -15,7 +15,6 @@ from shapely.ops import nearest_points, unary_union from game.theater import ControlPoint from game.utils import Distance, meters, nautical_miles -from gen import Conflict from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.flight import Flight @@ -27,9 +26,12 @@ ThreatPoly = Union[MultiPolygon, Polygon] class ThreatZones: - def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None: + def __init__( + self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats + ) -> None: self.airbases = airbases self.air_defenses = air_defenses + self.radar_sam_threats = radar_sam_threats self.all = unary_union([airbases, air_defenses]) def closest_boundary(self, point: DcsPoint) -> DcsPoint: @@ -83,6 +85,20 @@ class ThreatZones: LineString((self.dcs_to_shapely_point(p.position) for p in flight.points)) ) + @singledispatchmethod + def threatened_by_radar_sam(self, target) -> bool: + raise NotImplementedError + + @threatened_by_radar_sam.register + def _threatened_by_radar_sam_geom(self, position: BaseGeometry) -> bool: + return self.radar_sam_threats.intersects(position) + + @threatened_by_radar_sam.register + def _threatened_by_radar_sam_flight(self, flight: Flight) -> bool: + return self.threatened_by_radar_sam( + LineString((self.dcs_to_shapely_point(p.position) for p in flight.points)) + ) + @classmethod def closest_enemy_airbase( cls, location: ControlPoint, max_distance: Distance @@ -134,6 +150,7 @@ class ThreatZones: """ air_threats = [] air_defenses = [] + radar_sam_threats = [] for control_point in game.theater.controlpoints: if control_point.captured != player: continue @@ -151,9 +168,16 @@ class ThreatZones: point = ShapelyPoint(tgo.position.x, tgo.position.y) threat_zone = point.buffer(threat_range.meters) air_defenses.append(threat_zone) + radar_threat_range = tgo.threat_range(group, 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) + radar_sam_threats.append(threat_zone) return cls( - airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses) + airbases=unary_union(air_threats), + air_defenses=unary_union(air_defenses), + radar_sam_threats=unary_union(radar_sam_threats), ) @staticmethod diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 5105bf56..3b06481c 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -833,7 +833,7 @@ class CoalitionMissionPlanner: for flight in builder.package.flights: if self.threat_zones.threatened_by_aircraft(flight): threats[EscortType.AirToAir] = True - if self.threat_zones.threatened_by_air_defense(flight): + if self.threat_zones.threatened_by_radar_sam(flight): threats[EscortType.Sead] = True return threats diff --git a/qt_ui/widgets/map/mapmodel.py b/qt_ui/widgets/map/mapmodel.py index 27d2474e..7b6e7bc5 100644 --- a/qt_ui/widgets/map/mapmodel.py +++ b/qt_ui/widgets/map/mapmodel.py @@ -538,17 +538,20 @@ class ThreatZonesJs(QObject): fullChanged = Signal() aircraftChanged = Signal() airDefensesChanged = Signal() + radarSamsChanged = Signal() def __init__( self, full: List[List[LeafletLatLon]], aircraft: List[List[LeafletLatLon]], air_defenses: List[List[LeafletLatLon]], + radar_sams: List[List[LeafletLatLon]], ) -> None: super().__init__() self._full = full self._aircraft = aircraft self._air_defenses = air_defenses + self._radar_sams = radar_sams @Property(list, notify=fullChanged) def full(self) -> List[List[LeafletLatLon]]: @@ -562,6 +565,10 @@ class ThreatZonesJs(QObject): def airDefenses(self) -> List[List[LeafletLatLon]]: return self._air_defenses + @Property(list, notify=radarSamsChanged) + def radarSams(self) -> List[List[LeafletLatLon]]: + return self._radar_sams + @staticmethod def polys_to_leaflet( poly: Union[Polygon, MultiPolygon], theater: ConflictTheater @@ -578,11 +585,12 @@ class ThreatZonesJs(QObject): cls.polys_to_leaflet(zones.all, theater), cls.polys_to_leaflet(zones.airbases, theater), cls.polys_to_leaflet(zones.air_defenses, theater), + cls.polys_to_leaflet(zones.radar_sam_threats, theater), ) @classmethod def empty(cls) -> ThreatZonesJs: - return ThreatZonesJs([], [], []) + return ThreatZonesJs([], [], [], []) class ThreatZoneContainerJs(QObject): diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js index a6d29c6c..8a508a3a 100644 --- a/resources/ui/map/map.js +++ b/resources/ui/map/map.js @@ -163,10 +163,12 @@ const allFlightPlansLayer = L.layerGroup(); const blueFullThreatZones = L.layerGroup(); const blueAircraftThreatZones = L.layerGroup(); const blueAirDefenseThreatZones = L.layerGroup(); +const blueRadarSamThreatZones = L.layerGroup(); const redFullThreatZones = L.layerGroup(); const redAircraftThreatZones = L.layerGroup(); const redAirDefenseThreatZones = L.layerGroup(); +const redRadarSamThreatZones = L.layerGroup(); L.control .groupedLayers( @@ -198,12 +200,14 @@ L.control Full: blueFullThreatZones, Aircraft: blueAircraftThreatZones, "Air Defenses": blueAirDefenseThreatZones, + "Radar SAMs": blueRadarSamThreatZones, }, "Red Threat Zones": { Hide: L.layerGroup().addTo(map), Full: redFullThreatZones, Aircraft: redAircraftThreatZones, "Air Defenses": redAirDefenseThreatZones, + "Radar SAMs": redRadarSamThreatZones, }, }, { @@ -735,9 +739,11 @@ function drawThreatZones() { blueFullThreatZones.clearLayers(); blueAircraftThreatZones.clearLayers(); blueAirDefenseThreatZones.clearLayers(); + blueRadarSamThreatZones.clearLayers(); redFullThreatZones.clearLayers(); redAircraftThreatZones.clearLayers(); redAirDefenseThreatZones.clearLayers(); + redRadarSamThreatZones.clearLayers(); _drawThreatZones(game.threatZones.blue.full, blueFullThreatZones, true); _drawThreatZones( @@ -750,6 +756,11 @@ function drawThreatZones() { blueAirDefenseThreatZones, true ); + _drawThreatZones( + game.threatZones.blue.radarSams, + blueRadarSamThreatZones, + true + ); _drawThreatZones(game.threatZones.red.full, redFullThreatZones, false); _drawThreatZones( @@ -762,6 +773,11 @@ function drawThreatZones() { redAirDefenseThreatZones, false ); + _drawThreatZones( + game.threatZones.red.radarSams, + redRadarSamThreatZones, + false + ); } function drawInitialMap() {