from __future__ import annotations from functools import singledispatchmethod from typing import TYPE_CHECKING, Union from dcs.mapping import Point as DcsPoint from shapely.geometry import ( LineString, MultiPolygon, Point as ShapelyPoint, Polygon, ) from shapely.geometry.base import BaseGeometry from shapely.ops import nearest_points, unary_union from game.utils import nautical_miles from gen.flights.flight import Flight if TYPE_CHECKING: from game import Game ThreatPoly = Union[MultiPolygon, Polygon] class ThreatZones: def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None: self.airbases = airbases self.air_defenses = air_defenses self.all = unary_union([airbases, air_defenses]) def closest_boundary(self, point: DcsPoint) -> DcsPoint: boundary, _ = nearest_points(self.all.boundary, self.dcs_to_shapely_point(point)) return DcsPoint(boundary.x, boundary.y) @singledispatchmethod def threatened(self, position) -> bool: raise NotImplementedError @threatened.register def _threatened_geometry(self, position: BaseGeometry) -> bool: return self.all.intersects(position) @threatened.register def _threatened_dcs_point(self, position: DcsPoint) -> bool: return self.all.intersects(self.dcs_to_shapely_point(position)) def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool: return self.threatened(LineString( [self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])) @singledispatchmethod def threatened_by_aircraft(self, target) -> bool: raise NotImplementedError @threatened_by_aircraft.register def _threatened_by_aircraft_geom(self, position: BaseGeometry) -> bool: return self.airbases.intersects(position) @threatened_by_aircraft.register def _threatened_by_aircraft_flight(self, flight: Flight) -> bool: return self.threatened_by_aircraft(LineString(( self.dcs_to_shapely_point(p.position) for p in flight.points ))) @singledispatchmethod def threatened_by_air_defense(self, target) -> bool: raise NotImplementedError @threatened_by_air_defense.register def _threatened_by_air_defense_geom(self, position: BaseGeometry) -> bool: return self.air_defenses.intersects(position) @threatened_by_air_defense.register def _threatened_by_air_defense_flight(self, flight: Flight) -> bool: return self.threatened_by_air_defense(LineString(( self.dcs_to_shapely_point(p.position) for p in flight.points ))) @classmethod def for_faction(cls, game: Game, player: bool) -> ThreatZones: """Generates the threat zones projected by the given coalition. Args: game: The game to generate the threat zone for. player: True if the coalition projecting the threat zone belongs to the player. Returns: The threat zones projected by the given coalition. If the threat zone belongs to the player, it is the zone that will be avoided by the enemy and vice versa. """ doctrine = game.faction_for(player).doctrine airbases = [] air_defenses = [] for control_point in game.theater.controlpoints: if control_point.captured != player: continue if control_point.runway_is_operational(): point = ShapelyPoint(control_point.position.x, control_point.position.y) cap_threat_range = (doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range) airbases.append(point.buffer(cap_threat_range.meters)) for tgo in control_point.ground_objects: for group in tgo.groups: threat_range = tgo.threat_range(group) # 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_defenses.append(threat_zone) return cls( airbases=unary_union(airbases), air_defenses=unary_union(air_defenses) ) @staticmethod def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint: return ShapelyPoint(point.x, point.y)