Improve detection of functional radar SAMs.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1109
This commit is contained in:
Dan Albert 2021-05-23 11:28:43 -07:00
parent eae0d6be94
commit ddd6e7d18f
6 changed files with 126 additions and 16 deletions

View File

@ -22,7 +22,38 @@ from dcs.ships import (
) )
from dcs.vehicles import AirDefence 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 # Radars
AirDefence.SAM_SA_19_Tunguska_Grison, AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL, AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
@ -74,4 +105,4 @@ UNITS_WITH_RADAR = [
Type_052B_Destroyer, Type_052B_Destroyer,
Type_054A_Frigate, Type_054A_Frigate,
Type_052C_Destroyer, Type_052C_Destroyer,
] }

View File

@ -8,9 +8,15 @@ 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 Group from dcs.unitgroup import Group
from dcs.unittype import VehicleType
from .. import db 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 from ..utils import Distance, meters
if TYPE_CHECKING: if TYPE_CHECKING:
@ -166,14 +172,7 @@ class TheaterGroundObject(MissionTarget):
def detection_range(self, group: Group) -> Distance: def detection_range(self, group: Group) -> Distance:
return self._max_range_of_type(group, "detection_range") return self._max_range_of_type(group, "detection_range")
def threat_range(self, group: Group) -> Distance: def threat_range(self, group: Group, radar_only: bool = False) -> 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") return self._max_range_of_type(group, "threat_range")
@property @property
@ -459,6 +458,38 @@ class SamGroundObject(BaseDefenseGroundObject):
def might_have_aa(self) -> bool: def might_have_aa(self) -> bool:
return True 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): class VehicleGroupGroundObject(BaseDefenseGroundObject):
def __init__( def __init__(

View File

@ -15,7 +15,6 @@ from shapely.ops import nearest_points, unary_union
from game.theater import ControlPoint from game.theater import ControlPoint
from game.utils import Distance, meters, nautical_miles from game.utils import Distance, meters, nautical_miles
from gen import Conflict
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight from gen.flights.flight import Flight
@ -27,9 +26,12 @@ ThreatPoly = Union[MultiPolygon, Polygon]
class ThreatZones: 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.airbases = airbases
self.air_defenses = air_defenses self.air_defenses = air_defenses
self.radar_sam_threats = radar_sam_threats
self.all = unary_union([airbases, air_defenses]) self.all = unary_union([airbases, air_defenses])
def closest_boundary(self, point: DcsPoint) -> DcsPoint: 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)) 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 @classmethod
def closest_enemy_airbase( def closest_enemy_airbase(
cls, location: ControlPoint, max_distance: Distance cls, location: ControlPoint, max_distance: Distance
@ -134,6 +150,7 @@ class ThreatZones:
""" """
air_threats = [] air_threats = []
air_defenses = [] air_defenses = []
radar_sam_threats = []
for control_point in game.theater.controlpoints: for control_point in game.theater.controlpoints:
if control_point.captured != player: if control_point.captured != player:
continue continue
@ -151,9 +168,16 @@ class ThreatZones:
point = ShapelyPoint(tgo.position.x, tgo.position.y) point = ShapelyPoint(tgo.position.x, tgo.position.y)
threat_zone = point.buffer(threat_range.meters) threat_zone = point.buffer(threat_range.meters)
air_defenses.append(threat_zone) 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( 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 @staticmethod

View File

@ -833,7 +833,7 @@ class CoalitionMissionPlanner:
for flight in builder.package.flights: for flight in builder.package.flights:
if self.threat_zones.threatened_by_aircraft(flight): if self.threat_zones.threatened_by_aircraft(flight):
threats[EscortType.AirToAir] = True 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 threats[EscortType.Sead] = True
return threats return threats

View File

@ -538,17 +538,20 @@ class ThreatZonesJs(QObject):
fullChanged = Signal() fullChanged = Signal()
aircraftChanged = Signal() aircraftChanged = Signal()
airDefensesChanged = Signal() airDefensesChanged = Signal()
radarSamsChanged = Signal()
def __init__( def __init__(
self, self,
full: List[List[LeafletLatLon]], full: List[List[LeafletLatLon]],
aircraft: List[List[LeafletLatLon]], aircraft: List[List[LeafletLatLon]],
air_defenses: List[List[LeafletLatLon]], air_defenses: List[List[LeafletLatLon]],
radar_sams: List[List[LeafletLatLon]],
) -> None: ) -> None:
super().__init__() super().__init__()
self._full = full self._full = full
self._aircraft = aircraft self._aircraft = aircraft
self._air_defenses = air_defenses self._air_defenses = air_defenses
self._radar_sams = radar_sams
@Property(list, notify=fullChanged) @Property(list, notify=fullChanged)
def full(self) -> List[List[LeafletLatLon]]: def full(self) -> List[List[LeafletLatLon]]:
@ -562,6 +565,10 @@ class ThreatZonesJs(QObject):
def airDefenses(self) -> List[List[LeafletLatLon]]: def airDefenses(self) -> List[List[LeafletLatLon]]:
return self._air_defenses return self._air_defenses
@Property(list, notify=radarSamsChanged)
def radarSams(self) -> List[List[LeafletLatLon]]:
return self._radar_sams
@staticmethod @staticmethod
def polys_to_leaflet( def polys_to_leaflet(
poly: Union[Polygon, MultiPolygon], theater: ConflictTheater 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.all, theater),
cls.polys_to_leaflet(zones.airbases, theater), cls.polys_to_leaflet(zones.airbases, theater),
cls.polys_to_leaflet(zones.air_defenses, theater), cls.polys_to_leaflet(zones.air_defenses, theater),
cls.polys_to_leaflet(zones.radar_sam_threats, theater),
) )
@classmethod @classmethod
def empty(cls) -> ThreatZonesJs: def empty(cls) -> ThreatZonesJs:
return ThreatZonesJs([], [], []) return ThreatZonesJs([], [], [], [])
class ThreatZoneContainerJs(QObject): class ThreatZoneContainerJs(QObject):

View File

@ -163,10 +163,12 @@ const allFlightPlansLayer = L.layerGroup();
const blueFullThreatZones = L.layerGroup(); const blueFullThreatZones = L.layerGroup();
const blueAircraftThreatZones = L.layerGroup(); const blueAircraftThreatZones = L.layerGroup();
const blueAirDefenseThreatZones = L.layerGroup(); const blueAirDefenseThreatZones = L.layerGroup();
const blueRadarSamThreatZones = L.layerGroup();
const redFullThreatZones = L.layerGroup(); const redFullThreatZones = L.layerGroup();
const redAircraftThreatZones = L.layerGroup(); const redAircraftThreatZones = L.layerGroup();
const redAirDefenseThreatZones = L.layerGroup(); const redAirDefenseThreatZones = L.layerGroup();
const redRadarSamThreatZones = L.layerGroup();
L.control L.control
.groupedLayers( .groupedLayers(
@ -198,12 +200,14 @@ L.control
Full: blueFullThreatZones, Full: blueFullThreatZones,
Aircraft: blueAircraftThreatZones, Aircraft: blueAircraftThreatZones,
"Air Defenses": blueAirDefenseThreatZones, "Air Defenses": blueAirDefenseThreatZones,
"Radar SAMs": blueRadarSamThreatZones,
}, },
"Red Threat Zones": { "Red Threat Zones": {
Hide: L.layerGroup().addTo(map), Hide: L.layerGroup().addTo(map),
Full: redFullThreatZones, Full: redFullThreatZones,
Aircraft: redAircraftThreatZones, Aircraft: redAircraftThreatZones,
"Air Defenses": redAirDefenseThreatZones, "Air Defenses": redAirDefenseThreatZones,
"Radar SAMs": redRadarSamThreatZones,
}, },
}, },
{ {
@ -735,9 +739,11 @@ function drawThreatZones() {
blueFullThreatZones.clearLayers(); blueFullThreatZones.clearLayers();
blueAircraftThreatZones.clearLayers(); blueAircraftThreatZones.clearLayers();
blueAirDefenseThreatZones.clearLayers(); blueAirDefenseThreatZones.clearLayers();
blueRadarSamThreatZones.clearLayers();
redFullThreatZones.clearLayers(); redFullThreatZones.clearLayers();
redAircraftThreatZones.clearLayers(); redAircraftThreatZones.clearLayers();
redAirDefenseThreatZones.clearLayers(); redAirDefenseThreatZones.clearLayers();
redRadarSamThreatZones.clearLayers();
_drawThreatZones(game.threatZones.blue.full, blueFullThreatZones, true); _drawThreatZones(game.threatZones.blue.full, blueFullThreatZones, true);
_drawThreatZones( _drawThreatZones(
@ -750,6 +756,11 @@ function drawThreatZones() {
blueAirDefenseThreatZones, blueAirDefenseThreatZones,
true true
); );
_drawThreatZones(
game.threatZones.blue.radarSams,
blueRadarSamThreatZones,
true
);
_drawThreatZones(game.threatZones.red.full, redFullThreatZones, false); _drawThreatZones(game.threatZones.red.full, redFullThreatZones, false);
_drawThreatZones( _drawThreatZones(
@ -762,6 +773,11 @@ function drawThreatZones() {
redAirDefenseThreatZones, redAirDefenseThreatZones,
false false
); );
_drawThreatZones(
game.threatZones.red.radarSams,
redRadarSamThreatZones,
false
);
} }
function drawInitialMap() { function drawInitialMap() {