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.
This commit is contained in:
Dan Albert 2023-08-10 00:03:30 -07:00 committed by Raffson
parent c50dcaaafb
commit a2630fc75f
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
24 changed files with 92 additions and 51 deletions

View File

@ -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]** Added support for the ARA Veinticinco de Mayo.
* **[Data]** Changed display name of the AI-only F-15E Strike Eagle for clarity. * **[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]** 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]** 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. * **[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+. * **[Mission Generation]** Configured target and initial points for F-15E S4+.

View File

@ -92,5 +92,5 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> AewcFlightPlan: def build(self, dump_debug_info: bool = False) -> AewcFlightPlan:
return AewcFlightPlan(self.flight, self.layout()) return AewcFlightPlan(self.flight, self.layout())

View File

@ -172,7 +172,7 @@ class Builder(FormationAttackBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
refuel=None, refuel=None,
) )
def build(self) -> AirAssaultFlightPlan: def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan:
return AirAssaultFlightPlan(self.flight, self.layout()) return AirAssaultFlightPlan(self.flight, self.layout())
def _generate_ctld_pickup(self) -> Point: def _generate_ctld_pickup(self) -> Point:

View File

@ -169,7 +169,7 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> AirliftFlightPlan: def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan:
return AirliftFlightPlan(self.flight, self.layout()) return AirliftFlightPlan(self.flight, self.layout())
def _generate_ctld_pickup(self) -> Point: def _generate_ctld_pickup(self) -> Point:

View File

@ -41,5 +41,5 @@ class Builder(FormationAttackBuilder[AntiShipFlightPlan, FormationAttackLayout])
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]: 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] 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()) return AntiShipFlightPlan(self.flight, self.layout())

View File

@ -39,5 +39,5 @@ class Builder(FormationAttackBuilder[BaiFlightPlan, FormationAttackLayout]):
return self._build(FlightWaypointType.INGRESS_BAI, targets) 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()) return BaiFlightPlan(self.flight, self.layout())

View File

@ -66,5 +66,5 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> BarCapFlightPlan: def build(self, dump_debug_info: bool = False) -> BarCapFlightPlan:
return BarCapFlightPlan(self.flight, self.layout()) return BarCapFlightPlan(self.flight, self.layout())

View File

@ -6,13 +6,16 @@ from datetime import timedelta
from typing import TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
from game.theater import FrontLine 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 .ibuilder import IBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .uizonedisplay import UiZone, UiZoneDisplay from .uizonedisplay import UiZone, UiZoneDisplay
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType from ..flightwaypointtype import FlightWaypointType
from ...flightplan.ipsolver import IpSolver
from ...persistence.paths import waypoint_debug_directory
if TYPE_CHECKING: if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint from ..flightwaypoint import FlightWaypoint
@ -20,13 +23,13 @@ if TYPE_CHECKING:
@dataclass @dataclass
class CasLayout(PatrollingLayout): class CasLayout(PatrollingLayout):
target: FlightWaypoint ingress: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure yield self.departure
yield from self.nav_to yield from self.nav_to
yield self.ingress
yield self.patrol_start yield self.patrol_start
yield self.target
yield self.patrol_end yield self.patrol_end
yield from self.nav_from yield from self.nav_from
yield self.arrival yield self.arrival
@ -59,7 +62,7 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
@property @property
def combat_speed_waypoints(self) -> set[FlightWaypoint]: 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: def request_escort_at(self) -> FlightWaypoint | None:
return self.layout.patrol_start return self.layout.patrol_start
@ -68,14 +71,17 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
return self.layout.patrol_end return self.layout.patrol_end
def ui_zone(self) -> UiZone: def ui_zone(self) -> UiZone:
midpoint = (
self.layout.patrol_start.position + self.layout.patrol_end.position
) / 2
return UiZone( return UiZone(
[self.layout.target.position], [midpoint],
self.engagement_distance, self.engagement_distance,
) )
class Builder(IBuilder[CasFlightPlan, CasLayout]): class Builder(IBuilder[CasFlightPlan, CasLayout]):
def layout(self) -> CasLayout: def layout(self, dump_debug_info: bool) -> CasLayout:
location = self.package.target location = self.package.target
if not isinstance(location, FrontLine): if not isinstance(location, FrontLine):
@ -92,10 +98,10 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
center = bounds.center center = bounds.center
egress = bounds.right_position egress = bounds.right_position
ingress_distance = ingress.distance_to_point(self.flight.departure.position) start_distance = ingress.distance_to_point(self.flight.departure.position)
egress_distance = egress.distance_to_point(self.flight.departure.position) end_distance = egress.distance_to_point(self.flight.departure.position)
if egress_distance < ingress_distance: if end_distance < start_distance:
ingress, egress = egress, ingress patrol_start, patrol_end = ingress, egress
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
@ -105,31 +111,65 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
if not is_helo if not is_helo
else feet(self.coalition.game.settings.heli_combat_alt_agl) 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( return CasLayout(
departure=builder.takeoff(self.flight.departure), departure=builder.takeoff(self.flight.departure),
nav_to=builder.nav_path( nav_to=builder.nav_path(
self.flight.departure.position, self.flight.departure.position,
ingress, ingress_point,
ingress_egress_altitude, patrol_altitude,
use_agl_ingress_egress, use_agl_patrol_altitude,
), ),
nav_from=builder.nav_path( nav_from=builder.nav_path(
egress, patrol_end,
self.flight.arrival.position, self.flight.arrival.position,
ingress_egress_altitude, patrol_altitude,
use_agl_ingress_egress, use_agl_patrol_altitude,
), ),
patrol_start=builder.ingress( ingress=ingress,
FlightWaypointType.INGRESS_CAS, ingress, location patrol_start=patrol_start_waypoint,
), patrol_end=patrol_end_waypoint,
target=builder.cas(center),
patrol_end=builder.egress(egress, location),
arrival=builder.land(self.flight.arrival), arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert), divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> CasFlightPlan: def build(self, dump_debug_info: bool = False) -> CasFlightPlan:
return CasFlightPlan(self.flight, self.layout()) return CasFlightPlan(self.flight, self.layout(dump_debug_info))

View File

@ -72,5 +72,5 @@ class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints) 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()) return CustomFlightPlan(self.flight, self.layout())

View File

@ -37,5 +37,5 @@ class Builder(FormationAttackBuilder[DeadFlightPlan, FormationAttackLayout]):
return self._build(FlightWaypointType.INGRESS_DEAD) 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()) return DeadFlightPlan(self.flight, self.layout())

View File

@ -117,5 +117,5 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> EscortFlightPlan: def build(self, dump_debug_info: bool = False) -> EscortFlightPlan:
return EscortFlightPlan(self.flight, self.layout()) return EscortFlightPlan(self.flight, self.layout())

View File

@ -78,5 +78,5 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
nav_from=[], nav_from=[],
) )
def build(self) -> FerryFlightPlan: def build(self, dump_debug_info: bool = False) -> FerryFlightPlan:
return FerryFlightPlan(self.flight, self.layout()) return FerryFlightPlan(self.flight, self.layout())

View File

@ -35,7 +35,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
def regenerate(self, dump_debug_info: bool = False) -> None: def regenerate(self, dump_debug_info: bool = False) -> None:
try: try:
self._generate_package_waypoints_if_needed(dump_debug_info) 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: except NavMeshError as ex:
color = "blue" if self.flight.squadron.player else "red" color = "blue" if self.flight.squadron.player else "red"
raise PlanningError( raise PlanningError(
@ -59,11 +59,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
return self.flight.departure.theater return self.flight.departure.theater
@abstractmethod @abstractmethod
def layout(self) -> LayoutT: def build(self, dump_debug_info: bool = False) -> FlightPlanT:
...
@abstractmethod
def build(self) -> FlightPlanT:
... ...
@property @property

View File

@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayou
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT) 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()) return OcaAircraftFlightPlan(self.flight, self.layout())

View File

@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaRunwayFlightPlan, FormationAttackLayout]
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY) 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()) return OcaRunwayFlightPlan(self.flight, self.layout())

View File

@ -121,5 +121,5 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> PackageRefuelingFlightPlan: def build(self, dump_debug_info: bool = False) -> PackageRefuelingFlightPlan:
return PackageRefuelingFlightPlan(self.flight, self.layout()) return PackageRefuelingFlightPlan(self.flight, self.layout())

View File

@ -89,5 +89,5 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
nav_from=[], nav_from=[],
) )
def build(self) -> RtbFlightPlan: def build(self, dump_debug_info: bool = False) -> RtbFlightPlan:
return RtbFlightPlan(self.flight, self.layout()) return RtbFlightPlan(self.flight, self.layout())

View File

@ -24,5 +24,5 @@ class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):
def layout(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
return self._build(FlightWaypointType.INGRESS_SEAD) 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()) return SeadFlightPlan(self.flight, self.layout())

View File

@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
return self._build(FlightWaypointType.INGRESS_STRIKE, targets) 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()) return StrikeFlightPlan(self.flight, self.layout())

View File

@ -134,5 +134,5 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
target, origin, ip, join, self.coalition, self.theater target, origin, ip, join, self.coalition, self.theater
).find_best_hold_point() ).find_best_hold_point()
def build(self) -> SweepFlightPlan: def build(self, dump_debug_info: bool = False) -> SweepFlightPlan:
return SweepFlightPlan(self.flight, self.layout()) return SweepFlightPlan(self.flight, self.layout())

View File

@ -130,5 +130,5 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> TarCapFlightPlan: def build(self, dump_debug_info: bool = False) -> TarCapFlightPlan:
return TarCapFlightPlan(self.flight, self.layout()) return TarCapFlightPlan(self.flight, self.layout())

View File

@ -81,5 +81,5 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def build(self) -> TheaterRefuelingFlightPlan: def build(self, dump_debug_info: bool = False) -> TheaterRefuelingFlightPlan:
return TheaterRefuelingFlightPlan(self.flight, self.layout()) return TheaterRefuelingFlightPlan(self.flight, self.layout())

View File

@ -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_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_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) INGRESS_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there CAS = 8 # Unused.
EGRESS = 9 # Should stop attack EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there LANDING_POINT = 11 # Should land there

View File

@ -11,9 +11,13 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
class CasIngressBuilder(PydcsWaypointBuilder): class CasIngressBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None: def add_tasks(self, waypoint: MovingPoint) -> None:
if isinstance(self.flight.flight_plan, CasFlightPlan): 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( waypoint.add_task(
EngageTargetsInZone( EngageTargetsInZone(
position=self.flight.flight_plan.layout.target.position, position=patrol_center,
radius=int(self.flight.flight_plan.engagement_distance.meters), radius=int(self.flight.flight_plan.engagement_distance.meters),
targets=[ targets=[
Targets.All.GroundUnits.GroundVehicles, Targets.All.GroundUnits.GroundVehicles,