Autoplan Air-to-Air Escorts for AWACS & Tankers

This commit is contained in:
Raffson 2024-12-17 17:43:00 +01:00
parent d2fa027cdd
commit e02698d8a8
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
11 changed files with 80 additions and 16 deletions

View File

@ -39,6 +39,7 @@
* **[Mission Generation]** Automatic datalink network setup for applicable aircraft (_should_ in theory avoid the need to re-save the mission) * **[Mission Generation]** Automatic datalink network setup for applicable aircraft (_should_ in theory avoid the need to re-save the mission)
* **[Options]** New option to force-enable deck-crew for super-carriers on dedicated server. * **[Options]** New option to force-enable deck-crew for super-carriers on dedicated server.
* **[Mission Generation]** Enable Supercarrier's LSO & Airboss stations * **[Mission Generation]** Enable Supercarrier's LSO & Airboss stations
* **[Autoplanner]** Plan Air-to-Air Escorts for AWACS & Tankers
## Fixes ## Fixes
* **[UI/UX]** A-10A flights can be edited again * **[UI/UX]** A-10A flights can be edited again

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List from typing import List
from game.ato import FlightType
from game.ato.package import Package from game.ato.package import Package
@ -11,6 +12,16 @@ class AirTaskingOrder:
#: The set of all planned packages in the ATO. #: The set of all planned packages in the ATO.
packages: List[Package] = field(default_factory=list) packages: List[Package] = field(default_factory=list)
@property
def has_awacs_package(self) -> bool:
return any(
[
p
for p in self.packages
if any([f for f in p.flights if f.flight_type is FlightType.AEWC])
]
)
def add_package(self, package: Package) -> None: def add_package(self, package: Package) -> None:
"""Adds a package to the ATO.""" """Adds a package to the ATO."""
self.packages.append(package) self.packages.append(package)

View File

@ -11,6 +11,7 @@ from .formationattack import (
) )
from .waypointbuilder import WaypointBuilder from .waypointbuilder import WaypointBuilder
from .. import FlightType from .. import FlightType
from ..packagewaypoints import PackageWaypoints
from ...utils import feet from ...utils import feet
@ -22,16 +23,28 @@ class EscortFlightPlan(FormationAttackFlightPlan):
class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]): class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
def layout(self) -> FormationAttackLayout: def layout(self) -> FormationAttackLayout:
assert self.package.waypoints is not None non_formation_escort = False
if self.package.waypoints is None:
self.package.waypoints = PackageWaypoints.create(
self.package, self.coalition, dump_debug_info=False
)
if self.package.primary_flight:
departure = self.package.primary_flight.flight_plan.layout.departure
self.package.waypoints.join = departure.position.lerp(
self.package.target.position, 0.2
)
non_formation_escort = True
builder = WaypointBuilder(self.flight) builder = WaypointBuilder(self.flight)
ingress, target = builder.escort( ingress, target = builder.escort(
self.package.waypoints.ingress, self.package.target self.package.waypoints.ingress, self.package.target
) )
if non_formation_escort:
target.position = self.package.waypoints.join
ingress.only_for_player = True ingress.only_for_player = True
target.only_for_player = True target.only_for_player = True
hold = None hold = None
if not self.flight.is_helo: if not (self.flight.is_helo or non_formation_escort):
hold = builder.hold(self._hold_point()) hold = builder.hold(self._hold_point())
join_pos = ( join_pos = (

View File

@ -4,6 +4,7 @@ from typing import Optional, TYPE_CHECKING
from game.theater import ControlPoint, MissionTarget, OffMapSpawn from game.theater import ControlPoint, MissionTarget, OffMapSpawn
from game.utils import nautical_miles from game.utils import nautical_miles
from ..ato import FlightType
from ..ato.flight import Flight from ..ato.flight import Flight
from ..ato.package import Package from ..ato.package import Package
from ..ato.starttype import StartType from ..ato.starttype import StartType
@ -46,10 +47,18 @@ class PackageBuilder:
caller should return any previously planned flights to the inventory caller should return any previously planned flights to the inventory
using release_planned_aircraft. using release_planned_aircraft.
""" """
target = self.package.target
heli = False
pf = self.package.primary_flight pf = self.package.primary_flight
heli = pf.is_helo if pf else False if pf:
target = (
pf.departure
if pf.flight_type in [FlightType.AEWC, FlightType.REFUELING]
else target
)
heli = pf.is_helo
squadron = self.air_wing.best_squadron_for( squadron = self.air_wing.best_squadron_for(
self.package.target, target,
plan.task, plan.task,
plan.num_aircraft, plan.num_aircraft,
heli, heli,

View File

@ -83,12 +83,19 @@ class PackageFulfiller:
purchase_multiplier: int, purchase_multiplier: int,
ignore_range: bool = False, ignore_range: bool = False,
) -> None: ) -> None:
target = mission.location
pf = builder.package.primary_flight
if (
pf
and pf.flight_type in [FlightType.AEWC, FlightType.REFUELING]
and flight.task is FlightType.ESCORT
):
target = pf.departure
if not builder.plan_flight(flight, ignore_range): if not builder.plan_flight(flight, ignore_range):
pf = builder.package.primary_flight
heli = pf.is_helo if pf else False heli = pf.is_helo if pf else False
missing_types.add(flight.task) missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest( purchase_order = AircraftProcurementRequest(
near=mission.location, near=target,
task_capability=flight.task, task_capability=flight.task,
number=flight.num_aircraft * purchase_multiplier, number=flight.num_aircraft * purchase_multiplier,
heli=heli, heli=heli,

View File

@ -102,8 +102,14 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
state.context.settings, state.context.settings,
) )
with state.context.tracer.trace(f"{color} {self.flights[0].task} planning"): with state.context.tracer.trace(f"{color} {self.flights[0].task} planning"):
asap = False
if (
not state.context.coalition.ato.has_awacs_package
and FlightType.AEWC in [f.task for f in self.flights]
):
asap = True
self.package = fulfiller.plan_mission( self.package = fulfiller.plan_mission(
ProposedMission(self.target, self.flights), ProposedMission(self.target, self.flights, asap=asap),
self.purchase_multiplier, self.purchase_multiplier,
state.context.now, state.context.now,
state.context.tracer, state.context.tracer,

View File

@ -25,6 +25,7 @@ class PlanAewc(PackagePlanningTask[MissionTarget]):
def propose_flights(self) -> None: def propose_flights(self) -> None:
self.propose_flight(FlightType.AEWC, 1) self.propose_flight(FlightType.AEWC, 1)
self.propose_flight(FlightType.ESCORT, 2)
@property @property
def asap(self) -> bool: def asap(self) -> bool:

View File

@ -25,3 +25,4 @@ class PlanRefueling(PackagePlanningTask[MissionTarget]):
def propose_flights(self) -> None: def propose_flights(self) -> None:
self.propose_flight(FlightType.REFUELING, 1) self.propose_flight(FlightType.REFUELING, 1)
self.propose_flight(FlightType.ESCORT, 2)

View File

@ -6,6 +6,7 @@ from collections.abc import Iterator, Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
import numpy as np
from dcs.mapping import heading_between_points from dcs.mapping import heading_between_points
from shapely.geometry import Point, MultiPolygon, Polygon from shapely.geometry import Point, MultiPolygon, Polygon
from shapely.geometry.base import BaseGeometry as Geometry, BaseGeometry from shapely.geometry.base import BaseGeometry as Geometry, BaseGeometry
@ -232,11 +233,12 @@ class WaypointStrategy:
min_distance_from_threat_to_target_buffer = target.buffer( min_distance_from_threat_to_target_buffer = target.buffer(
target_size.meters target_size.meters
).distance(self.threat_zones.boundary) ).distance(self.threat_zones.boundary)
threat_mask = self.threat_zones.buffer( if np.isfinite(min_distance_from_threat_to_target_buffer).all():
-min_distance_from_threat_to_target_buffer - wiggle.meters threat_mask = self.threat_zones.buffer(
) -min_distance_from_threat_to_target_buffer - wiggle.meters
self._threat_tolerance = ThreatTolerance(target, target_size, wiggle) )
self.threat_zones = self.threat_zones.difference(threat_mask) self._threat_tolerance = ThreatTolerance(target, target_size, wiggle)
self.threat_zones = self.threat_zones.difference(threat_mask)
def nearest(self, point: Point) -> None: def nearest(self, point: Point) -> None:
if self.point_for_nearest_solution is not None: if self.point_for_nearest_solution is not None:

View File

@ -689,6 +689,16 @@ class Settings:
max=100, max=100,
detail="See 2-ship weight factor (WF4)", detail="See 2-ship weight factor (WF4)",
) )
primary_task_distance_factor: int = bounded_int_option(
"Primary task distance weight (NM)",
CAMPAIGN_MANAGEMENT_PAGE,
FLIGHT_PLANNER_AUTOMATION,
default=75,
min=10,
max=250,
detail="A larger number will force the auto-planner to stick with squadrons that have a matching primary task."
" A smaller number will ignore squadrons with a matching primary task that are too far out.",
)
# Mission Generator # Mission Generator
# Gameplay # Gameplay

View File

@ -10,6 +10,7 @@ from .squadrondefloader import SquadronDefLoader
from ..campaignloader.squadrondefgenerator import SquadronDefGenerator from ..campaignloader.squadrondefgenerator import SquadronDefGenerator
from ..factions.faction import Faction from ..factions.faction import Faction
from ..theater import ControlPoint, MissionTarget from ..theater import ControlPoint, MissionTarget
from ..utils import Distance
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@ -87,10 +88,12 @@ class AirWing:
ordered, ordered,
key=lambda s: ( key=lambda s: (
# This looks like the opposite of what we want because False sorts # This looks like the opposite of what we want because False sorts
# before True. # before True. Distance is also added,
s.primary_task != task, # i.e. 75NM with primary task match is similar to non-primary with 0NM to target
best_aircraft.index(s.aircraft), int(s.primary_task != task)
s.location.distance_to(location), + Distance.from_meters(s.location.distance_to(location)).nautical_miles
/ self.settings.primary_task_distance_factor
+ best_aircraft.index(s.aircraft) / len(best_aircraft),
), ),
) )