mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Use navmesh to plan strike-like flight plans.
The cases where the target is extremely close to the origin point still use the old flight plan pattern. This is probably fine. https://github.com/Khopa/dcs_liberation/issues/292
This commit is contained in:
parent
d95f623ca9
commit
81af5d7497
@ -5,7 +5,7 @@ Saves from 2.3 are not compatible with 2.4.
|
||||
## Features/Improvements
|
||||
|
||||
* **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats.
|
||||
* **[Flight Planner]** BARCAP, TARCAP, CAS, and Fighter Sweep flights will now navigate around threat areas en route to the target area when practical. More types coming soon.
|
||||
* **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical.
|
||||
|
||||
# 2.3.3
|
||||
|
||||
|
||||
@ -208,8 +208,11 @@ class NavMesh:
|
||||
points.append(ShapelyPoint(cp.position.x, cp.position.y))
|
||||
for tgo in cp.ground_objects:
|
||||
points.append(ShapelyPoint(tgo.position.x, tgo.position.y))
|
||||
return box(*LineString(points).bounds).buffer(nautical_miles(60).meters,
|
||||
resolution=1)
|
||||
# Needs to be a large enough boundary beyond the known points so that
|
||||
# threatened airbases at the map edges have room to retreat from the
|
||||
# threat without running off the navmesh.
|
||||
return box(*LineString(points).bounds).buffer(
|
||||
nautical_miles(100).meters, resolution=1)
|
||||
|
||||
@staticmethod
|
||||
def create_navpolys(polys: List[Polygon],
|
||||
|
||||
@ -11,9 +11,9 @@ from shapely.geometry import (
|
||||
Polygon,
|
||||
)
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
from shapely.ops import unary_union
|
||||
from shapely.ops import nearest_points, unary_union
|
||||
|
||||
from game.utils import nautical_miles
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -29,9 +29,23 @@ class ThreatZones:
|
||||
self.air_defenses = air_defenses
|
||||
self.all = unary_union([airbases, air_defenses])
|
||||
|
||||
def threatened(self, position: BaseGeometry) -> bool:
|
||||
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)]))
|
||||
|
||||
@ -501,27 +501,27 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
class StrikeFlightPlan(FormationFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
hold: FlightWaypoint
|
||||
nav_to: List[FlightWaypoint]
|
||||
join: FlightWaypoint
|
||||
ingress: FlightWaypoint
|
||||
targets: List[FlightWaypoint]
|
||||
egress: FlightWaypoint
|
||||
split: FlightWaypoint
|
||||
nav_from: List[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.hold,
|
||||
self.join,
|
||||
self.ingress
|
||||
]
|
||||
yield self.takeoff
|
||||
yield self.hold
|
||||
yield from self.nav_to
|
||||
yield self.join
|
||||
yield self.ingress
|
||||
yield from self.targets
|
||||
yield from [
|
||||
self.egress,
|
||||
self.split,
|
||||
self.land,
|
||||
]
|
||||
yield self.egress
|
||||
yield self.split
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@ -728,6 +728,7 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
faction = self.game.enemy_faction
|
||||
self.doctrine: Doctrine = faction.doctrine
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def populate_flight_plan(
|
||||
self, flight: Flight,
|
||||
@ -773,12 +774,79 @@ class FlightPlanBuilder:
|
||||
f"{task} flight plan generation not implemented")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
ingress_point = self._ingress_point()
|
||||
egress_point = self._egress_point()
|
||||
# The simple case is where the target is greater than the ingress
|
||||
# distance into the threat zone and the target is not near the departure
|
||||
# airfield. In this case, we can plan the shortest route from the
|
||||
# departure airfield to the target, use the last non-threatened point as
|
||||
# the join point, and plan the IP inside the threatened area.
|
||||
#
|
||||
# When the target is near the edge of the threat zone the IP may need to
|
||||
# be placed outside the zone.
|
||||
#
|
||||
# +--------------+ +---------------+
|
||||
# | | | |
|
||||
# | | IP---+-T |
|
||||
# | | | |
|
||||
# | | | |
|
||||
# +--------------+ +---------------+
|
||||
#
|
||||
# Here we want to place the IP first and route the flight to the IP
|
||||
# rather than routing to the target and placing the IP based on the join
|
||||
# point.
|
||||
#
|
||||
# The other case that we need to handle is when the target is close to
|
||||
# the origin airfield. In this case we also need to set up the IP first,
|
||||
# but depending on the placement of the IP we may need to place the join
|
||||
# point in a retreating position.
|
||||
#
|
||||
# A messy (and very unlikely) case that we can't do much about:
|
||||
#
|
||||
# +--------------+ +---------------+
|
||||
# | | | |
|
||||
# | IP-+---+-T |
|
||||
# | | | |
|
||||
# | | | |
|
||||
# +--------------+ +---------------+
|
||||
from gen.ato import PackageWaypoints
|
||||
target = self.package.target.position
|
||||
|
||||
join_point = self.preferred_join_point()
|
||||
if join_point is None:
|
||||
# The whole path from the origin airfield to the target is
|
||||
# threatened. Need to retreat out of the threat area.
|
||||
join_point = self.retreat_point(self.package_airfield().position)
|
||||
|
||||
attack_heading = join_point.heading_between_point(target)
|
||||
ingress_point = self._ingress_point(attack_heading)
|
||||
join_distance = meters(join_point.distance_to_point(target))
|
||||
ingress_distance = meters(ingress_point.distance_to_point(target))
|
||||
if join_distance < ingress_distance:
|
||||
# The second case described above. The ingress point is farther from
|
||||
# the target than the join point. Use the fallback behavior for now.
|
||||
self.legacy_package_waypoints_impl()
|
||||
return
|
||||
|
||||
# The first case described above. The ingress and join points are placed
|
||||
# reasonably relative to each other.
|
||||
egress_point = self._egress_point(attack_heading)
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
WaypointBuilder.perturb(join_point),
|
||||
ingress_point,
|
||||
egress_point,
|
||||
WaypointBuilder.perturb(join_point),
|
||||
)
|
||||
|
||||
def retreat_point(self, origin: Point) -> Point:
|
||||
return self.threat_zones.closest_boundary(origin)
|
||||
|
||||
def legacy_package_waypoints_impl(self) -> None:
|
||||
from gen.ato import PackageWaypoints
|
||||
ingress_point = self._ingress_point(
|
||||
self._target_heading_to_package_airfield())
|
||||
egress_point = self._egress_point(
|
||||
self._target_heading_to_package_airfield())
|
||||
join_point = self._rendezvous_point(ingress_point)
|
||||
split_point = self._rendezvous_point(egress_point)
|
||||
|
||||
from gen.ato import PackageWaypoints
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
join_point,
|
||||
ingress_point,
|
||||
@ -786,6 +854,14 @@ class FlightPlanBuilder:
|
||||
split_point,
|
||||
)
|
||||
|
||||
def preferred_join_point(self) -> Optional[Point]:
|
||||
path = self.game.navmesh_for(self.is_player).shortest_path(
|
||||
self.package_airfield().position, self.package.target.position)
|
||||
for point in reversed(path):
|
||||
if not self.threat_zones.threatened(point):
|
||||
return point
|
||||
return None
|
||||
|
||||
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||
"""Generates a strike flight plan.
|
||||
|
||||
@ -1142,18 +1218,25 @@ class FlightPlanBuilder:
|
||||
ingress, target, egress = builder.escort(
|
||||
self.package.waypoints.ingress, self.package.target,
|
||||
self.package.waypoints.egress)
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
hold=hold,
|
||||
hold_duration=timedelta(minutes=5),
|
||||
join=builder.join(self.package.waypoints.join),
|
||||
nav_to=builder.nav_path(hold.position, join.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
join=join,
|
||||
ingress=ingress,
|
||||
targets=[target],
|
||||
egress=egress,
|
||||
split=builder.split(self.package.waypoints.split),
|
||||
split=split,
|
||||
nav_from=builder.nav_path(split.position, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
@ -1295,18 +1378,26 @@ class FlightPlanBuilder:
|
||||
target_waypoints.append(
|
||||
self.target_area_waypoint(flight, location, builder))
|
||||
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
hold=hold,
|
||||
hold_duration=timedelta(minutes=5),
|
||||
join=builder.join(self.package.waypoints.join),
|
||||
nav_to=builder.nav_path(hold.position, join.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
join=join,
|
||||
ingress=builder.ingress(ingress_type,
|
||||
self.package.waypoints.ingress, location),
|
||||
targets=target_waypoints,
|
||||
egress=builder.egress(self.package.waypoints.egress, location),
|
||||
split=builder.split(self.package.waypoints.split),
|
||||
split=split,
|
||||
nav_from=builder.nav_path(split.position, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
@ -1347,16 +1438,14 @@ class FlightPlanBuilder:
|
||||
return self._retreating_rendezvous_point(attack_transition)
|
||||
return self._advancing_rendezvous_point(attack_transition)
|
||||
|
||||
def _ingress_point(self) -> Point:
|
||||
heading = self._target_heading_to_package_airfield()
|
||||
def _ingress_point(self, heading: int) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 + 25, self.doctrine.ingress_egress_distance.meters
|
||||
heading - 180 + 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _egress_point(self) -> Point:
|
||||
heading = self._target_heading_to_package_airfield()
|
||||
def _egress_point(self, heading: int) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 - 25, self.doctrine.ingress_egress_distance.meters
|
||||
heading - 180 - 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _target_heading_to_package_airfield(self) -> int:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user