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
This commit is contained in:
Dan Albert
2020-12-21 13:46:18 -08:00
parent 66149bb591
commit bff905fae5
9 changed files with 630 additions and 36 deletions

View File

@@ -450,17 +450,21 @@ class CasFlightPlan(PatrollingFlightPlan):
@dataclass(frozen=True)
class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
nav_to: List[FlightWaypoint]
nav_from: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
lead_time: timedelta
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
yield from [
self.takeoff,
self.patrol_start,
self.patrol_end,
self.land,
]
yield from self.nav_from
yield self.land
if self.divert is not None:
yield self.divert
@@ -876,7 +880,7 @@ class FlightPlanBuilder:
int(self.doctrine.max_patrol_altitude.meters)
))
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.race_track(start, end, patrol_alt)
return BarCapFlightPlan(
@@ -903,7 +907,7 @@ class FlightPlanBuilder:
start = target.point_from_heading(heading,
-self.doctrine.sweep_distance.meters)
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.sweep(start, target,
self.doctrine.ingress_altitude)
@@ -1000,7 +1004,7 @@ class FlightPlanBuilder:
int(self.doctrine.max_patrol_altitude.meters)))
# Create points
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder = WaypointBuilder(flight, self.game, self.is_player)
if isinstance(location, FrontLine):
orbit0p, orbit1p = self.racetrack_for_frontline(location)
@@ -1019,6 +1023,10 @@ class FlightPlanBuilder:
patrol_duration=self.doctrine.cap_duration,
engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, orbit0p,
patrol_alt),
nav_from=builder.nav_path(orbit1p, flight.arrival.position,
patrol_alt),
patrol_start=start,
patrol_end=end,
land=builder.land(flight.arrival),
@@ -1113,7 +1121,7 @@ class FlightPlanBuilder:
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
assert self.package.waypoints is not None
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder = WaypointBuilder(flight, self.game, self.is_player)
ingress, target, egress = builder.escort(
self.package.waypoints.ingress, self.package.target,
self.package.waypoints.egress)
@@ -1151,7 +1159,7 @@ class FlightPlanBuilder:
center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance)
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder = WaypointBuilder(flight, self.game, self.is_player)
return CasFlightPlan(
package=self.package,
@@ -1247,7 +1255,7 @@ class FlightPlanBuilder:
flight: The flight to generate the landing waypoint for.
arrival: Arrival airfield or carrier.
"""
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
builder = WaypointBuilder(flight, self.game, self.is_player)
return builder.land(arrival)
def strike_flightplan(
@@ -1255,8 +1263,7 @@ class FlightPlanBuilder:
ingress_type: FlightWaypointType,
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
assert self.package.waypoints is not None
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine,
targets)
builder = WaypointBuilder(flight, self.game, self.is_player, targets)
target_waypoints: List[FlightWaypoint] = []
if targets is not None:

View File

@@ -1,21 +1,31 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import List, Optional, Tuple, Union
from typing import (
Iterable,
Iterator,
List,
Optional,
TYPE_CHECKING,
Tuple,
Union,
)
from dcs.mapping import Point
from dcs.unit import Unit
from dcs.unitgroup import VehicleGroup
from game.data.doctrine import Doctrine
if TYPE_CHECKING:
from game import Game
from game.theater import (
ControlPoint,
MissionTarget,
OffMapSpawn,
TheaterGroundObject,
)
from game.utils import Distance, meters
from game.weather import Conditions
from game.utils import Distance, meters, nautical_miles
from .flight import Flight, FlightWaypoint, FlightWaypointType
@@ -26,12 +36,13 @@ class StrikeTarget:
class WaypointBuilder:
def __init__(self, conditions: Conditions, flight: Flight,
doctrine: Doctrine,
def __init__(self, flight: Flight, game: Game, player: bool,
targets: Optional[List[StrikeTarget]] = None) -> None:
self.conditions = conditions
self.flight = flight
self.doctrine = doctrine
self.conditions = game.conditions
self.doctrine = game.faction_for(player).doctrine
self.threat_zones = game.threat_zone_for(not player)
self.navmesh = game.navmesh_for(player)
self.targets = targets
@property
@@ -429,3 +440,80 @@ class WaypointBuilder:
egress = self.egress(egress, target)
return ingress, waypoint, egress
@staticmethod
def nav(position: Point, altitude: Distance) -> FlightWaypoint:
"""Creates a navigation point.
Args:
position: Position of the waypoint.
altitude: Altitude of the waypoint.
"""
waypoint = FlightWaypoint(
FlightWaypointType.NAV,
position.x,
position.y,
altitude
)
waypoint.name = "NAV"
waypoint.description = ""
waypoint.pretty_name = ""
return waypoint
def nav_path(self, a: Point, b: Point,
altitude: Distance) -> List[FlightWaypoint]:
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
return [self.nav(self.perturb(p), altitude) for p in path]
def clean_nav_points(self, points: Iterable[Point]) -> Iterator[Point]:
# Examine a sliding window of three waypoints. `current` is the waypoint
# being checked for prunability. `previous` is the last emitted waypoint
# before `current`. `nxt` is the waypoint after `current`.
previous: Optional[Point] = None
current: Optional[Point] = None
for nxt in points:
if current is None:
current = nxt
continue
if previous is None:
previous = current
current = nxt
continue
if self.nav_point_prunable(previous, current, nxt):
current = nxt
continue
yield current
previous = current
current = nxt
def nav_point_prunable(self, previous: Point, current: Point,
nxt: Point) -> bool:
previous_threatened = self.threat_zones.path_threatened(previous,
current)
next_threatened = self.threat_zones.path_threatened(current, nxt)
pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
previous_distance = meters(previous.distance_to_point(current))
distance = meters(current.distance_to_point(nxt))
distance_without = previous_distance + distance
if distance > distance_without:
# Don't prune paths to make them longer.
return False
# We could shorten the path by removing the intermediate
# waypoint. Do so if the new path isn't higher threat.
if not pruned_threatened:
# The new path is not threatened, so safe to prune.
return True
# The new path is threatened. Only allow if both paths were
# threatened anyway.
return previous_threatened and next_threatened
@staticmethod
def perturb(point: Point) -> Point:
deviation = nautical_miles(1)
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
return Point(point.x + x_adj, point.y + y_adj)