mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +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
|
## 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]** 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
|
# 2.3.3
|
||||||
|
|
||||||
|
|||||||
@ -208,8 +208,11 @@ class NavMesh:
|
|||||||
points.append(ShapelyPoint(cp.position.x, cp.position.y))
|
points.append(ShapelyPoint(cp.position.x, cp.position.y))
|
||||||
for tgo in cp.ground_objects:
|
for tgo in cp.ground_objects:
|
||||||
points.append(ShapelyPoint(tgo.position.x, tgo.position.y))
|
points.append(ShapelyPoint(tgo.position.x, tgo.position.y))
|
||||||
return box(*LineString(points).bounds).buffer(nautical_miles(60).meters,
|
# Needs to be a large enough boundary beyond the known points so that
|
||||||
resolution=1)
|
# 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
|
@staticmethod
|
||||||
def create_navpolys(polys: List[Polygon],
|
def create_navpolys(polys: List[Polygon],
|
||||||
|
|||||||
@ -11,9 +11,9 @@ from shapely.geometry import (
|
|||||||
Polygon,
|
Polygon,
|
||||||
)
|
)
|
||||||
from shapely.geometry.base import BaseGeometry
|
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
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -29,9 +29,23 @@ class ThreatZones:
|
|||||||
self.air_defenses = air_defenses
|
self.air_defenses = air_defenses
|
||||||
self.all = unary_union([airbases, 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)
|
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:
|
def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool:
|
||||||
return self.threatened(LineString(
|
return self.threatened(LineString(
|
||||||
[self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)]))
|
[self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)]))
|
||||||
|
|||||||
@ -501,27 +501,27 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
|||||||
class StrikeFlightPlan(FormationFlightPlan):
|
class StrikeFlightPlan(FormationFlightPlan):
|
||||||
takeoff: FlightWaypoint
|
takeoff: FlightWaypoint
|
||||||
hold: FlightWaypoint
|
hold: FlightWaypoint
|
||||||
|
nav_to: List[FlightWaypoint]
|
||||||
join: FlightWaypoint
|
join: FlightWaypoint
|
||||||
ingress: FlightWaypoint
|
ingress: FlightWaypoint
|
||||||
targets: List[FlightWaypoint]
|
targets: List[FlightWaypoint]
|
||||||
egress: FlightWaypoint
|
egress: FlightWaypoint
|
||||||
split: FlightWaypoint
|
split: FlightWaypoint
|
||||||
|
nav_from: List[FlightWaypoint]
|
||||||
land: FlightWaypoint
|
land: FlightWaypoint
|
||||||
divert: Optional[FlightWaypoint]
|
divert: Optional[FlightWaypoint]
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
yield from [
|
yield self.takeoff
|
||||||
self.takeoff,
|
yield self.hold
|
||||||
self.hold,
|
yield from self.nav_to
|
||||||
self.join,
|
yield self.join
|
||||||
self.ingress
|
yield self.ingress
|
||||||
]
|
|
||||||
yield from self.targets
|
yield from self.targets
|
||||||
yield from [
|
yield self.egress
|
||||||
self.egress,
|
yield self.split
|
||||||
self.split,
|
yield from self.nav_from
|
||||||
self.land,
|
yield self.land
|
||||||
]
|
|
||||||
if self.divert is not None:
|
if self.divert is not None:
|
||||||
yield self.divert
|
yield self.divert
|
||||||
|
|
||||||
@ -728,6 +728,7 @@ class FlightPlanBuilder:
|
|||||||
else:
|
else:
|
||||||
faction = self.game.enemy_faction
|
faction = self.game.enemy_faction
|
||||||
self.doctrine: Doctrine = faction.doctrine
|
self.doctrine: Doctrine = faction.doctrine
|
||||||
|
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
|
|
||||||
def populate_flight_plan(
|
def populate_flight_plan(
|
||||||
self, flight: Flight,
|
self, flight: Flight,
|
||||||
@ -773,12 +774,79 @@ class FlightPlanBuilder:
|
|||||||
f"{task} flight plan generation not implemented")
|
f"{task} flight plan generation not implemented")
|
||||||
|
|
||||||
def regenerate_package_waypoints(self) -> None:
|
def regenerate_package_waypoints(self) -> None:
|
||||||
ingress_point = self._ingress_point()
|
# The simple case is where the target is greater than the ingress
|
||||||
egress_point = self._egress_point()
|
# 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)
|
join_point = self._rendezvous_point(ingress_point)
|
||||||
split_point = self._rendezvous_point(egress_point)
|
split_point = self._rendezvous_point(egress_point)
|
||||||
|
|
||||||
from gen.ato import PackageWaypoints
|
|
||||||
self.package.waypoints = PackageWaypoints(
|
self.package.waypoints = PackageWaypoints(
|
||||||
join_point,
|
join_point,
|
||||||
ingress_point,
|
ingress_point,
|
||||||
@ -786,6 +854,14 @@ class FlightPlanBuilder:
|
|||||||
split_point,
|
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:
|
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||||
"""Generates a strike flight plan.
|
"""Generates a strike flight plan.
|
||||||
|
|
||||||
@ -1142,18 +1218,25 @@ class FlightPlanBuilder:
|
|||||||
ingress, target, egress = builder.escort(
|
ingress, target, egress = builder.escort(
|
||||||
self.package.waypoints.ingress, self.package.target,
|
self.package.waypoints.ingress, self.package.target,
|
||||||
self.package.waypoints.egress)
|
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(
|
return StrikeFlightPlan(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
flight=flight,
|
flight=flight,
|
||||||
takeoff=builder.takeoff(flight.departure),
|
takeoff=builder.takeoff(flight.departure),
|
||||||
hold=builder.hold(self._hold_point(flight)),
|
hold=hold,
|
||||||
hold_duration=timedelta(minutes=5),
|
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,
|
ingress=ingress,
|
||||||
targets=[target],
|
targets=[target],
|
||||||
egress=egress,
|
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),
|
land=builder.land(flight.arrival),
|
||||||
divert=builder.divert(flight.divert)
|
divert=builder.divert(flight.divert)
|
||||||
)
|
)
|
||||||
@ -1295,18 +1378,26 @@ class FlightPlanBuilder:
|
|||||||
target_waypoints.append(
|
target_waypoints.append(
|
||||||
self.target_area_waypoint(flight, location, builder))
|
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(
|
return StrikeFlightPlan(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
flight=flight,
|
flight=flight,
|
||||||
takeoff=builder.takeoff(flight.departure),
|
takeoff=builder.takeoff(flight.departure),
|
||||||
hold=builder.hold(self._hold_point(flight)),
|
hold=hold,
|
||||||
hold_duration=timedelta(minutes=5),
|
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,
|
ingress=builder.ingress(ingress_type,
|
||||||
self.package.waypoints.ingress, location),
|
self.package.waypoints.ingress, location),
|
||||||
targets=target_waypoints,
|
targets=target_waypoints,
|
||||||
egress=builder.egress(self.package.waypoints.egress, location),
|
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),
|
land=builder.land(flight.arrival),
|
||||||
divert=builder.divert(flight.divert)
|
divert=builder.divert(flight.divert)
|
||||||
)
|
)
|
||||||
@ -1347,16 +1438,14 @@ class FlightPlanBuilder:
|
|||||||
return self._retreating_rendezvous_point(attack_transition)
|
return self._retreating_rendezvous_point(attack_transition)
|
||||||
return self._advancing_rendezvous_point(attack_transition)
|
return self._advancing_rendezvous_point(attack_transition)
|
||||||
|
|
||||||
def _ingress_point(self) -> Point:
|
def _ingress_point(self, heading: int) -> Point:
|
||||||
heading = self._target_heading_to_package_airfield()
|
|
||||||
return self.package.target.position.point_from_heading(
|
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:
|
def _egress_point(self, heading: int) -> Point:
|
||||||
heading = self._target_heading_to_package_airfield()
|
|
||||||
return self.package.target.position.point_from_heading(
|
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:
|
def _target_heading_to_package_airfield(self) -> int:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user