dcs-retribution/game/threatzones.py
Dan Albert bff905fae5 Use navmeshes to improve TARCAP flight plans.
Started with TARCAP because they're simple, but will follow and extend
this to the other flight plans next.

This works by building navigation meshes (navmeshes) of the theater
based on the threat regions. A navmesh is created for each faction to
allow the unique pathing around each side's threats. Navmeshes are built
such that there are nav edges around threat zones to allow the planner
to pick waypoints that (slightly) route around threats before
approaching the target.

Using the navmesh, routes are found using A*. Performance appears
adequate, and could probably be improved with a cache if needed since
the small number of origin points means many flights will share portions
of their flight paths.

This adds a few visual debugging tools to the map. They're disabled by
default, but changing the local `debug` variable in `DisplayOptions` to
`True` will make them appear in the display options menu. These are:

* Display navmeshes (red and blue). Displaying either navmesh will draw
  each navmesh polygon on the map view and highlight the mesh that
  contains the cursor. Neighbors are indicated by a small yellow line
  pointing from the center of the polygon's edge/vertext that is shared
  with its neighbor toward the centroid of the zone.
* Shortest path from control point to mouse location. The first control
  point for the selected faction is arbitrarily selected, and the
  shortest path from that control point to the mouse cursor will be
  drawn on the map.
* TARCAP plan near mouse location. A TARCAP will be planned from the
  faction's first control point to the target nearest the mouse cursor.

https://github.com/Khopa/dcs_liberation/issues/292
2020-12-23 17:09:34 -08:00

99 lines
3.5 KiB
Python

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 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 threatened(self, position: BaseGeometry) -> bool:
return self.all.intersects(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:
opposing_doctrine = game.faction_for(not 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 = (opposing_doctrine.cap_max_distance_from_cp +
opposing_doctrine.cap_engagement_range)
airbases.append(point.buffer(cap_threat_range.meters))
for tgo in control_point.ground_objects:
threat_range = tgo.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_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)