From a2630fc75f1603129f58fd0685cf95f039b1ed19 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 10 Aug 2023 00:03:30 -0700 Subject: [PATCH] Add a real CAS ingress point. Putting the ingress point directly on one end of the FLOT means that AI flights won't start searching and engaging targets until they reach that point. If the front line has advanced toward the flight's departure airfield, it might overfly targets on its way to the IP. Instead, place an IP for CAS the same way we place any other IP. The AI will fly to that and start searching from there. This also: * Removes the midpoint waypoint, since it didn't serve any real purpose * Names the FLOT boundary waypoints for what they actually are Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2231. --- changelog.md | 1 + game/ato/flightplans/aewc.py | 2 +- game/ato/flightplans/airassault.py | 2 +- game/ato/flightplans/airlift.py | 2 +- game/ato/flightplans/antiship.py | 2 +- game/ato/flightplans/bai.py | 2 +- game/ato/flightplans/barcap.py | 2 +- game/ato/flightplans/cas.py | 88 ++++++++++++++----- game/ato/flightplans/custom.py | 2 +- game/ato/flightplans/dead.py | 2 +- game/ato/flightplans/escort.py | 2 +- game/ato/flightplans/ferry.py | 2 +- game/ato/flightplans/ibuilder.py | 8 +- game/ato/flightplans/ocaaircraft.py | 2 +- game/ato/flightplans/ocarunway.py | 2 +- game/ato/flightplans/packagerefueling.py | 2 +- game/ato/flightplans/rtb.py | 2 +- game/ato/flightplans/sead.py | 2 +- game/ato/flightplans/strike.py | 2 +- game/ato/flightplans/sweep.py | 2 +- game/ato/flightplans/tarcap.py | 2 +- game/ato/flightplans/theaterrefueling.py | 2 +- game/ato/flightwaypointtype.py | 2 +- .../aircraft/waypoints/casingress.py | 6 +- 24 files changed, 92 insertions(+), 51 deletions(-) diff --git a/changelog.md b/changelog.md index c5766228..a7bc582e 100644 --- a/changelog.md +++ b/changelog.md @@ -213,6 +213,7 @@ BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions * **[Data]** Added support for the ARA Veinticinco de Mayo. * **[Data]** Changed display name of the AI-only F-15E Strike Eagle for clarity. * **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone. +* **[Flight Planning]** Moved CAS ingress point off the front line so that the AI begins their target search earlier. * **[Flight Planning]** Loadouts and aircraft properties can now be set per-flight member. Warning: AI flights should not use mixed loadouts. * **[Flight Planning]** Laser codes that are pre-assigned to weapons at mission start can now be chosen from a list in the loadout UI. This does not affect the aircraft's TGP, just the weapons. Currently only implemented for the F-15E S4+ and F-16C. * **[Mission Generation]** Configured target and initial points for F-15E S4+. diff --git a/game/ato/flightplans/aewc.py b/game/ato/flightplans/aewc.py index d2f3e495..8e4885a9 100644 --- a/game/ato/flightplans/aewc.py +++ b/game/ato/flightplans/aewc.py @@ -92,5 +92,5 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]): bullseye=builder.bullseye(), ) - def build(self) -> AewcFlightPlan: + def build(self, dump_debug_info: bool = False) -> AewcFlightPlan: return AewcFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/airassault.py b/game/ato/flightplans/airassault.py index ace2850e..463bb747 100644 --- a/game/ato/flightplans/airassault.py +++ b/game/ato/flightplans/airassault.py @@ -172,7 +172,7 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]): refuel=None, ) - def build(self) -> AirAssaultFlightPlan: + def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan: return AirAssaultFlightPlan(self.flight, self.layout()) def _generate_ctld_pickup(self) -> Point: diff --git a/game/ato/flightplans/airlift.py b/game/ato/flightplans/airlift.py index 1ec88736..55969aa5 100644 --- a/game/ato/flightplans/airlift.py +++ b/game/ato/flightplans/airlift.py @@ -169,7 +169,7 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]): bullseye=builder.bullseye(), ) - def build(self) -> AirliftFlightPlan: + def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan: return AirliftFlightPlan(self.flight, self.layout()) def _generate_ctld_pickup(self) -> Point: diff --git a/game/ato/flightplans/antiship.py b/game/ato/flightplans/antiship.py index 59d4f89f..b883fd9f 100644 --- a/game/ato/flightplans/antiship.py +++ b/game/ato/flightplans/antiship.py @@ -41,5 +41,5 @@ class Builder(FormationAttackBuilder[AntiShipFlightPlan, FormationAttackLayout]) def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]: return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups] - def build(self) -> AntiShipFlightPlan: + def build(self, dump_debug_info: bool = False) -> AntiShipFlightPlan: return AntiShipFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/bai.py b/game/ato/flightplans/bai.py index e64e27ac..22b8f915 100644 --- a/game/ato/flightplans/bai.py +++ b/game/ato/flightplans/bai.py @@ -39,5 +39,5 @@ class Builder(FormationAttackBuilder[BaiFlightPlan, FormationAttackLayout]): return self._build(FlightWaypointType.INGRESS_BAI, targets) - def build(self) -> BaiFlightPlan: + def build(self, dump_debug_info: bool = False) -> BaiFlightPlan: return BaiFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/barcap.py b/game/ato/flightplans/barcap.py index 9e59fb65..1fdf10c1 100644 --- a/game/ato/flightplans/barcap.py +++ b/game/ato/flightplans/barcap.py @@ -66,5 +66,5 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]): bullseye=builder.bullseye(), ) - def build(self) -> BarCapFlightPlan: + def build(self, dump_debug_info: bool = False) -> BarCapFlightPlan: return BarCapFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/cas.py b/game/ato/flightplans/cas.py index 973ba23e..be6bd5c2 100644 --- a/game/ato/flightplans/cas.py +++ b/game/ato/flightplans/cas.py @@ -6,13 +6,16 @@ from datetime import timedelta from typing import TYPE_CHECKING, Type from game.theater import FrontLine -from game.utils import Distance, Speed, kph, feet, nautical_miles +from game.utils import Distance, Speed, kph, dcs_to_shapely_point +from game.utils import feet, nautical_miles from .ibuilder import IBuilder from .invalidobjectivelocation import InvalidObjectiveLocation from .patrolling import PatrollingFlightPlan, PatrollingLayout from .uizonedisplay import UiZone, UiZoneDisplay from .waypointbuilder import WaypointBuilder from ..flightwaypointtype import FlightWaypointType +from ...flightplan.ipsolver import IpSolver +from ...persistence.paths import waypoint_debug_directory if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint @@ -20,13 +23,13 @@ if TYPE_CHECKING: @dataclass class CasLayout(PatrollingLayout): - target: FlightWaypoint + ingress: FlightWaypoint def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.departure yield from self.nav_to + yield self.ingress yield self.patrol_start - yield self.target yield self.patrol_end yield from self.nav_from yield self.arrival @@ -59,7 +62,7 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay): @property def combat_speed_waypoints(self) -> set[FlightWaypoint]: - return {self.layout.patrol_start, self.layout.target, self.layout.patrol_end} + return {self.layout.ingress, self.layout.patrol_start, self.layout.patrol_end} def request_escort_at(self) -> FlightWaypoint | None: return self.layout.patrol_start @@ -68,14 +71,17 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay): return self.layout.patrol_end def ui_zone(self) -> UiZone: + midpoint = ( + self.layout.patrol_start.position + self.layout.patrol_end.position + ) / 2 return UiZone( - [self.layout.target.position], + [midpoint], self.engagement_distance, ) class Builder(IBuilder[CasFlightPlan, CasLayout]): - def layout(self) -> CasLayout: + def layout(self, dump_debug_info: bool) -> CasLayout: location = self.package.target if not isinstance(location, FrontLine): @@ -92,10 +98,10 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]): center = bounds.center egress = bounds.right_position - ingress_distance = ingress.distance_to_point(self.flight.departure.position) - egress_distance = egress.distance_to_point(self.flight.departure.position) - if egress_distance < ingress_distance: - ingress, egress = egress, ingress + start_distance = ingress.distance_to_point(self.flight.departure.position) + end_distance = egress.distance_to_point(self.flight.departure.position) + if end_distance < start_distance: + patrol_start, patrol_end = ingress, egress builder = WaypointBuilder(self.flight) @@ -105,31 +111,65 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]): if not is_helo else feet(self.coalition.game.settings.heli_combat_alt_agl) ) - use_agl_ingress_egress = is_helo + use_agl_patrol_altitude = is_helo + + ip_solver = IpSolver( + dcs_to_shapely_point(self.flight.departure.position), + dcs_to_shapely_point(ingress), + self.doctrine, + self.threat_zones.all, + ) + ip_solver.set_debug_properties( + waypoint_debug_directory() / "IP", self.theater.terrain + ) + ingress_point_shapely = ip_solver.solve() + if dump_debug_info: + ip_solver.dump_debug_info() + + ingress_point = ingress.new_in_same_map( + ingress_point_shapely.x, ingress_point_shapely.y + ) + + patrol_start_waypoint = builder.nav( + ingress, ingress_egress_altitude, use_agl_patrol_altitude + ) + patrol_start_waypoint.name = "FLOT START" + patrol_start_waypoint.pretty_name = "FLOT start" + patrol_start_waypoint.description = "FLOT boundary" + + patrol_end_waypoint = builder.nav( + egress, ingress_egress_altitude, use_agl_patrol_altitude + ) + patrol_end_waypoint.name = "FLOT END" + patrol_end_waypoint.pretty_name = "FLOT end" + patrol_end_waypoint.description = "FLOT boundary" + + ingress = builder.ingress( + FlightWaypointType.INGRESS_CAS, ingress_point, location + ) + ingress.description = f"Ingress to provide CAS at {location}" return CasLayout( departure=builder.takeoff(self.flight.departure), nav_to=builder.nav_path( self.flight.departure.position, - ingress, - ingress_egress_altitude, - use_agl_ingress_egress, + ingress_point, + patrol_altitude, + use_agl_patrol_altitude, ), nav_from=builder.nav_path( - egress, + patrol_end, self.flight.arrival.position, - ingress_egress_altitude, - use_agl_ingress_egress, + patrol_altitude, + use_agl_patrol_altitude, ), - patrol_start=builder.ingress( - FlightWaypointType.INGRESS_CAS, ingress, location - ), - target=builder.cas(center), - patrol_end=builder.egress(egress, location), + ingress=ingress, + patrol_start=patrol_start_waypoint, + patrol_end=patrol_end_waypoint, arrival=builder.land(self.flight.arrival), divert=builder.divert(self.flight.divert), bullseye=builder.bullseye(), ) - def build(self) -> CasFlightPlan: - return CasFlightPlan(self.flight, self.layout()) + def build(self, dump_debug_info: bool = False) -> CasFlightPlan: + return CasFlightPlan(self.flight, self.layout(dump_debug_info)) diff --git a/game/ato/flightplans/custom.py b/game/ato/flightplans/custom.py index 36b5a336..1befb56c 100644 --- a/game/ato/flightplans/custom.py +++ b/game/ato/flightplans/custom.py @@ -72,5 +72,5 @@ class Builder(IBuilder[CustomFlightPlan, CustomLayout]): builder = WaypointBuilder(self.flight) return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints) - def build(self) -> CustomFlightPlan: + def build(self, dump_debug_info: bool = False) -> CustomFlightPlan: return CustomFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/dead.py b/game/ato/flightplans/dead.py index 6827130d..7f240ced 100644 --- a/game/ato/flightplans/dead.py +++ b/game/ato/flightplans/dead.py @@ -37,5 +37,5 @@ class Builder(FormationAttackBuilder[DeadFlightPlan, FormationAttackLayout]): return self._build(FlightWaypointType.INGRESS_DEAD) - def build(self) -> DeadFlightPlan: + def build(self, dump_debug_info: bool = False) -> DeadFlightPlan: return DeadFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/escort.py b/game/ato/flightplans/escort.py index f2a8824b..1879c14a 100644 --- a/game/ato/flightplans/escort.py +++ b/game/ato/flightplans/escort.py @@ -117,5 +117,5 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]): bullseye=builder.bullseye(), ) - def build(self) -> EscortFlightPlan: + def build(self, dump_debug_info: bool = False) -> EscortFlightPlan: return EscortFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/ferry.py b/game/ato/flightplans/ferry.py index b694def7..1520d3cc 100644 --- a/game/ato/flightplans/ferry.py +++ b/game/ato/flightplans/ferry.py @@ -78,5 +78,5 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]): nav_from=[], ) - def build(self) -> FerryFlightPlan: + def build(self, dump_debug_info: bool = False) -> FerryFlightPlan: return FerryFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/ibuilder.py b/game/ato/flightplans/ibuilder.py index b85589e4..a91cd6f3 100644 --- a/game/ato/flightplans/ibuilder.py +++ b/game/ato/flightplans/ibuilder.py @@ -35,7 +35,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]): def regenerate(self, dump_debug_info: bool = False) -> None: try: self._generate_package_waypoints_if_needed(dump_debug_info) - self._flight_plan = self.build() + self._flight_plan = self.build(dump_debug_info) except NavMeshError as ex: color = "blue" if self.flight.squadron.player else "red" raise PlanningError( @@ -59,11 +59,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]): return self.flight.departure.theater @abstractmethod - def layout(self) -> LayoutT: - ... - - @abstractmethod - def build(self) -> FlightPlanT: + def build(self, dump_debug_info: bool = False) -> FlightPlanT: ... @property diff --git a/game/ato/flightplans/ocaaircraft.py b/game/ato/flightplans/ocaaircraft.py index 652f3d3a..4f24e041 100644 --- a/game/ato/flightplans/ocaaircraft.py +++ b/game/ato/flightplans/ocaaircraft.py @@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayou return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT) - def build(self) -> OcaAircraftFlightPlan: + def build(self, dump_debug_info: bool = False) -> OcaAircraftFlightPlan: return OcaAircraftFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/ocarunway.py b/game/ato/flightplans/ocarunway.py index fd7b3bfd..234d8f41 100644 --- a/game/ato/flightplans/ocarunway.py +++ b/game/ato/flightplans/ocarunway.py @@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaRunwayFlightPlan, FormationAttackLayout] return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY) - def build(self) -> OcaRunwayFlightPlan: + def build(self, dump_debug_info: bool = False) -> OcaRunwayFlightPlan: return OcaRunwayFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/packagerefueling.py b/game/ato/flightplans/packagerefueling.py index 34317959..68e7269e 100644 --- a/game/ato/flightplans/packagerefueling.py +++ b/game/ato/flightplans/packagerefueling.py @@ -121,5 +121,5 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]): bullseye=builder.bullseye(), ) - def build(self) -> PackageRefuelingFlightPlan: + def build(self, dump_debug_info: bool = False) -> PackageRefuelingFlightPlan: return PackageRefuelingFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/rtb.py b/game/ato/flightplans/rtb.py index 663c3624..c8025a0a 100644 --- a/game/ato/flightplans/rtb.py +++ b/game/ato/flightplans/rtb.py @@ -89,5 +89,5 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]): nav_from=[], ) - def build(self) -> RtbFlightPlan: + def build(self, dump_debug_info: bool = False) -> RtbFlightPlan: return RtbFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/sead.py b/game/ato/flightplans/sead.py index 3c6e89ae..2a691026 100644 --- a/game/ato/flightplans/sead.py +++ b/game/ato/flightplans/sead.py @@ -24,5 +24,5 @@ class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]): def layout(self) -> FormationAttackLayout: return self._build(FlightWaypointType.INGRESS_SEAD) - def build(self) -> SeadFlightPlan: + def build(self, dump_debug_info: bool = False) -> SeadFlightPlan: return SeadFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/strike.py b/game/ato/flightplans/strike.py index 303c50b6..e04387c3 100644 --- a/game/ato/flightplans/strike.py +++ b/game/ato/flightplans/strike.py @@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]): return self._build(FlightWaypointType.INGRESS_STRIKE, targets) - def build(self) -> StrikeFlightPlan: + def build(self, dump_debug_info: bool = False) -> StrikeFlightPlan: return StrikeFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/sweep.py b/game/ato/flightplans/sweep.py index e039d0da..68e7e0b7 100644 --- a/game/ato/flightplans/sweep.py +++ b/game/ato/flightplans/sweep.py @@ -134,5 +134,5 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]): target, origin, ip, join, self.coalition, self.theater ).find_best_hold_point() - def build(self) -> SweepFlightPlan: + def build(self, dump_debug_info: bool = False) -> SweepFlightPlan: return SweepFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/tarcap.py b/game/ato/flightplans/tarcap.py index 9f8689f8..21ea3ef0 100644 --- a/game/ato/flightplans/tarcap.py +++ b/game/ato/flightplans/tarcap.py @@ -130,5 +130,5 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]): bullseye=builder.bullseye(), ) - def build(self) -> TarCapFlightPlan: + def build(self, dump_debug_info: bool = False) -> TarCapFlightPlan: return TarCapFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightplans/theaterrefueling.py b/game/ato/flightplans/theaterrefueling.py index bdc120b5..7f3e8ccc 100644 --- a/game/ato/flightplans/theaterrefueling.py +++ b/game/ato/flightplans/theaterrefueling.py @@ -81,5 +81,5 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]): bullseye=builder.bullseye(), ) - def build(self) -> TheaterRefuelingFlightPlan: + def build(self, dump_debug_info: bool = False) -> TheaterRefuelingFlightPlan: return TheaterRefuelingFlightPlan(self.flight, self.layout()) diff --git a/game/ato/flightwaypointtype.py b/game/ato/flightwaypointtype.py index f9913e8a..8ba5024c 100644 --- a/game/ato/flightwaypointtype.py +++ b/game/ato/flightwaypointtype.py @@ -25,7 +25,7 @@ class FlightWaypointType(IntEnum): INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points) INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points) INGRESS_CAS = 7 # Ingress cas (should start CAS task) - CAS = 8 # Should do CAS there + CAS = 8 # Unused. EGRESS = 9 # Should stop attack DESCENT_POINT = 10 # Should start descending to pattern alt LANDING_POINT = 11 # Should land there diff --git a/game/missiongenerator/aircraft/waypoints/casingress.py b/game/missiongenerator/aircraft/waypoints/casingress.py index af891c5c..8e4c859b 100644 --- a/game/missiongenerator/aircraft/waypoints/casingress.py +++ b/game/missiongenerator/aircraft/waypoints/casingress.py @@ -11,9 +11,13 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder class CasIngressBuilder(PydcsWaypointBuilder): def add_tasks(self, waypoint: MovingPoint) -> None: if isinstance(self.flight.flight_plan, CasFlightPlan): + patrol_center = ( + self.flight.flight_plan.layout.patrol_start.position + + self.flight.flight_plan.layout.patrol_end.position + ) / 2 waypoint.add_task( EngageTargetsInZone( - position=self.flight.flight_plan.layout.target.position, + position=patrol_center, radius=int(self.flight.flight_plan.engagement_distance.meters), targets=[ Targets.All.GroundUnits.GroundVehicles,