diff --git a/changelog.md b/changelog.md index a3570323..b5b938aa 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,9 @@ * **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom * **[Factions]** Added map Persian Gulf full by Plob +## Fixes : +* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned. + # 2.2.0 ## Features/Improvements : diff --git a/game/data/doctrine.py b/game/data/doctrine.py index 8d3e1a91..99bb254a 100644 --- a/game/data/doctrine.py +++ b/game/data/doctrine.py @@ -16,6 +16,8 @@ class Doctrine: sead_max_range: int rendezvous_altitude: int + hold_distance: int + push_distance: int join_distance: int split_distance: int ingress_egress_distance: int @@ -44,6 +46,8 @@ MODERN_DOCTRINE = Doctrine( strike_max_range=1500000, sead_max_range=1500000, rendezvous_altitude=feet_to_meter(25000), + hold_distance=nm_to_meter(15), + push_distance=nm_to_meter(20), join_distance=nm_to_meter(20), split_distance=nm_to_meter(20), ingress_egress_distance=nm_to_meter(45), @@ -69,6 +73,8 @@ COLDWAR_DOCTRINE = Doctrine( strike_max_range=1500000, sead_max_range=1500000, rendezvous_altitude=feet_to_meter(22000), + hold_distance=nm_to_meter(10), + push_distance=nm_to_meter(10), join_distance=nm_to_meter(10), split_distance=nm_to_meter(10), ingress_egress_distance=nm_to_meter(30), @@ -93,6 +99,8 @@ WWII_DOCTRINE = Doctrine( antiship=True, strike_max_range=1500000, sead_max_range=1500000, + hold_distance=nm_to_meter(5), + push_distance=nm_to_meter(5), join_distance=nm_to_meter(5), split_distance=nm_to_meter(5), rendezvous_altitude=feet_to_meter(10000), diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index e0df2b01..c73eb3ae 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -7,6 +7,7 @@ generating the waypoints for the mission. """ from __future__ import annotations +import math from datetime import timedelta from functools import cached_property import logging @@ -275,18 +276,14 @@ class PatrollingFlightPlan(FlightPlan): @dataclass(frozen=True) class BarCapFlightPlan(PatrollingFlightPlan): takeoff: FlightWaypoint - ascent: FlightWaypoint - descent: FlightWaypoint land: FlightWaypoint @property def waypoints(self) -> List[FlightWaypoint]: return [ self.takeoff, - self.ascent, self.patrol_start, self.patrol_end, - self.descent, self.land, ] @@ -294,20 +291,16 @@ class BarCapFlightPlan(PatrollingFlightPlan): @dataclass(frozen=True) class CasFlightPlan(PatrollingFlightPlan): takeoff: FlightWaypoint - ascent: FlightWaypoint target: FlightWaypoint - descent: FlightWaypoint land: FlightWaypoint @property def waypoints(self) -> List[FlightWaypoint]: return [ self.takeoff, - self.ascent, self.patrol_start, self.target, self.patrol_end, - self.descent, self.land, ] @@ -321,18 +314,14 @@ class CasFlightPlan(PatrollingFlightPlan): @dataclass(frozen=True) class FrontLineCapFlightPlan(PatrollingFlightPlan): takeoff: FlightWaypoint - ascent: FlightWaypoint - descent: FlightWaypoint land: FlightWaypoint @property def waypoints(self) -> List[FlightWaypoint]: return [ self.takeoff, - self.ascent, self.patrol_start, self.patrol_end, - self.descent, self.land, ] @@ -360,28 +349,24 @@ class FrontLineCapFlightPlan(PatrollingFlightPlan): @dataclass(frozen=True) class StrikeFlightPlan(FormationFlightPlan): takeoff: FlightWaypoint - ascent: FlightWaypoint hold: FlightWaypoint join: FlightWaypoint ingress: FlightWaypoint targets: List[FlightWaypoint] egress: FlightWaypoint split: FlightWaypoint - descent: FlightWaypoint land: FlightWaypoint @property def waypoints(self) -> List[FlightWaypoint]: return [ self.takeoff, - self.ascent, self.hold, self.join, self.ingress ] + self.targets + [ self.egress, self.split, - self.descent, self.land, ] @@ -681,10 +666,8 @@ class FlightPlanBuilder: flight=flight, patrol_duration=self.doctrine.cap_duration, takeoff=builder.takeoff(flight.from_cp), - ascent=builder.ascent(flight.from_cp), patrol_start=start, patrol_end=end, - descent=descent, land=land ) @@ -736,10 +719,8 @@ class FlightPlanBuilder: # duration of the escorted mission, or until it is winchester/bingo. patrol_duration=self.doctrine.cap_duration, takeoff=builder.takeoff(flight.from_cp), - ascent=builder.ascent(flight.from_cp), patrol_start=start, patrol_end=end, - descent=descent, land=land ) @@ -805,14 +786,12 @@ class FlightPlanBuilder: package=self.package, flight=flight, takeoff=builder.takeoff(flight.from_cp), - ascent=builder.ascent(flight.from_cp), hold=builder.hold(self._hold_point(flight)), join=builder.join(self.package.waypoints.join), ingress=ingress, targets=[target], egress=egress, split=builder.split(self.package.waypoints.split), - descent=descent, land=land ) @@ -842,11 +821,9 @@ class FlightPlanBuilder: flight=flight, patrol_duration=self.doctrine.cas_duration, takeoff=builder.takeoff(flight.from_cp), - ascent=builder.ascent(flight.from_cp), patrol_start=builder.ingress_cas(ingress, location), target=builder.cas(center), patrol_end=builder.egress(egress, location), - descent=descent, land=land ) @@ -871,12 +848,50 @@ class FlightPlanBuilder: return builder.strike_area(location) def _hold_point(self, flight: Flight) -> Point: - heading = flight.from_cp.position.heading_between_point( - self.package.target.position - ) - return flight.from_cp.position.point_from_heading( - heading, nm_to_meter(15) + assert self.package.waypoints is not None + origin = flight.from_cp.position + target = self.package.target.position + join = self.package.waypoints.join + origin_to_target = origin.distance_to_point(target) + join_to_target = join.distance_to_point(target) + if origin_to_target < join_to_target: + # If the origin airfield is closer to the target than the join + # point, plan the hold point such that it retreats from the origin + # airfield. + return join.point_from_heading(target.heading_between_point(origin), + self.doctrine.push_distance) + + heading_to_join = origin.heading_between_point(join) + hold_point = origin.point_from_heading(heading_to_join, + self.doctrine.push_distance) + if hold_point.distance_to_point(join) >= self.doctrine.push_distance: + # Hold point is between the origin airfield and the join point and + # spaced sufficiently. + return hold_point + + # The hold point is between the origin airfield and the join point, but + # the distance between the hold point and the join point is too short. + # Bend the hold point out to extend the distance while maintaining the + # minimum distance from the origin airfield to keep the AI flying + # properly. + origin_to_join = origin.distance_to_point(join) + cos_theta = ( + (self.doctrine.hold_distance ** 2 + + origin_to_join ** 2 - + self.doctrine.join_distance ** 2) / + (2 * self.doctrine.hold_distance * origin_to_join) ) + try: + theta = math.acos(cos_theta) + except ValueError: + # No solution that maintains hold and join distances. Extend the + # hold point away from the target. + return origin.point_from_heading( + target.heading_between_point(origin), + self.doctrine.hold_distance) + + return origin.point_from_heading(heading_to_join - theta, + self.doctrine.hold_distance) # TODO: Make a model for the waypoint builder and use that in the UI. def generate_ascend_point(self, flight: Flight, @@ -944,23 +959,37 @@ class FlightPlanBuilder: package=self.package, flight=flight, takeoff=builder.takeoff(flight.from_cp), - ascent=builder.ascent(flight.from_cp), hold=builder.hold(self._hold_point(flight)), join=builder.join(self.package.waypoints.join), ingress=ingress, targets=target_waypoints, egress=builder.egress(self.package.waypoints.egress, location), split=builder.split(self.package.waypoints.split), - descent=descent, land=land ) def _join_point(self, ingress_point: Point) -> Point: + ingress_distance = self._distance_to_package_airfield(ingress_point) + if ingress_distance < self.doctrine.join_distance: + # If the ingress point is close to the origin, plan the join point + # farther back. + return ingress_point.point_from_heading( + self.package.target.position.heading_between_point( + self.package_airfield().position), + self.doctrine.join_distance) heading = self._heading_to_package_airfield(ingress_point) return ingress_point.point_from_heading(heading, -self.doctrine.join_distance) def _split_point(self, egress_point: Point) -> Point: + egress_distance = self._distance_to_package_airfield(egress_point) + if egress_distance < self.doctrine.split_distance: + # If the ingress point is close to the origin, plan the split point + # farther back. + return egress_point.point_from_heading( + self.package.target.position.heading_between_point( + self.package_airfield().position), + self.doctrine.split_distance) heading = self._heading_to_package_airfield(egress_point) return egress_point.point_from_heading(heading, -self.doctrine.split_distance) @@ -983,6 +1012,9 @@ class FlightPlanBuilder: def _heading_to_package_airfield(self, point: Point) -> int: return self.package_airfield().position.heading_between_point(point) + def _distance_to_package_airfield(self, point: Point) -> int: + return self.package_airfield().position.distance_to_point(point) + def package_airfield(self) -> ControlPoint: # We'll always have a package, but if this is being planned via the UI # it could be the first flight in the package.