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,