From 81af5d7497fe48c8e3e774e2c9bef929a118bdb7 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 23 Dec 2020 19:12:35 -0800 Subject: [PATCH] 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 --- changelog.md | 2 +- game/navmesh.py | 7 +- game/threatzones.py | 20 +++++- gen/flights/flightplan.py | 143 +++++++++++++++++++++++++++++++------- 4 files changed, 139 insertions(+), 33 deletions(-) diff --git a/changelog.md b/changelog.md index 0170c889..a945f4eb 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/game/navmesh.py b/game/navmesh.py index 8a8b8e8d..2a2ca6b4 100644 --- a/game/navmesh.py +++ b/game/navmesh.py @@ -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], diff --git a/game/threatzones.py b/game/threatzones.py index 17e276ae..61874e14 100644 --- a/game/threatzones.py +++ b/game/threatzones.py @@ -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)])) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 56f52e0a..a8d0ce03 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -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: