dcs_liberation/game/flightplan/holdzonegeometry.py
Dan Albert 9e2e4ffa74 Update pydcs, adapt to new Point APIs.
This is briefly moving us over to my fork of pydcs while we wait for
https://github.com/pydcs/dcs/pull/206 to be merged. The adaptation is
invasive enough that I don't want it lingering for long.
2022-02-23 01:02:48 +00:00

110 lines
4.2 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING
import shapely.ops
from dcs import Point
from shapely.geometry import MultiPolygon, Point as ShapelyPoint, Polygon
from game.utils import nautical_miles
if TYPE_CHECKING:
from game.coalition import Coalition
from game.theater import ConflictTheater
class HoldZoneGeometry:
"""Defines the zones used for finding optimal hold point placement.
The zones themselves are stored in the class rather than just the resulting hold
point so that the zones can be drawn in the map for debugging purposes.
"""
def __init__(
self,
target: Point,
home: Point,
ip: Point,
join: Point,
coalition: Coalition,
theater: ConflictTheater,
) -> None:
self._target = target
# Hold points are placed one of two ways. Either approach guarantees:
#
# * Safe hold point.
# * Minimum distance to the join point.
# * Not closer to the target than the join point.
#
# 1. As near the join point as possible with a specific distance from the
# departure airfield. This prevents loitering directly above the airfield but
# also keeps the hold point close to the departure airfield.
#
# 2. Alternatively, if the entire home zone is excluded by the above criteria,
# as neat the departure airfield as possible within a minimum distance from
# the join point, with a restricted turn angle at the join point. This
# handles the case where we need to backtrack from the departure airfield and
# the join point to place the hold point, but the turn angle limit restricts
# the maximum distance of the backtrack while maintaining the direction of
# the flight plan.
self.threat_zone = coalition.opponent.threat_zone.all
self.home = ShapelyPoint(home.x, home.y)
self.join = ShapelyPoint(join.x, join.y)
self.join_bubble = self.join.buffer(coalition.doctrine.push_distance.meters)
join_to_target_distance = join.distance_to_point(target)
self.target_bubble = ShapelyPoint(target.x, target.y).buffer(
join_to_target_distance
)
self.home_bubble = self.home.buffer(coalition.doctrine.hold_distance.meters)
excluded_zones = shapely.ops.unary_union(
[self.join_bubble, self.target_bubble, self.threat_zone]
)
if not isinstance(excluded_zones, MultiPolygon):
excluded_zones = MultiPolygon([excluded_zones])
self.excluded_zones = excluded_zones
join_heading = ip.heading_between_point(join)
# Arbitrarily large since this is later constrained by the map boundary, and
# we'll be picking a location close to the IP anyway. Just used to avoid real
# distance calculations to project to the map edge.
large_distance = nautical_miles(400).meters
turn_limit = 40
join_limit_ccw = join.point_from_heading(
join_heading - turn_limit, large_distance
)
join_limit_cw = join.point_from_heading(
join_heading + turn_limit, large_distance
)
join_direction_limit_wedge = Polygon(
[
(join.x, join.y),
(join_limit_ccw.x, join_limit_ccw.y),
(join_limit_cw.x, join_limit_cw.y),
]
)
permissible_zones = (
coalition.nav_mesh.map_bounds(theater)
.intersection(join_direction_limit_wedge)
.difference(self.excluded_zones)
.difference(self.home_bubble)
)
if not isinstance(permissible_zones, MultiPolygon):
permissible_zones = MultiPolygon([permissible_zones])
self.permissible_zones = permissible_zones
self.preferred_lines = self.home_bubble.boundary.difference(self.excluded_zones)
def find_best_hold_point(self) -> Point:
if self.preferred_lines.is_empty:
hold, _ = shapely.ops.nearest_points(self.permissible_zones, self.home)
else:
hold, _ = shapely.ops.nearest_points(self.preferred_lines, self.join)
return self._target.new_in_same_map(hold.x, hold.y)