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)
* **[Options]** New option to force-enable deck-crew for super-carriers on dedicated server.
* **[Mission Generation]** Enable Supercarrier's LSO & Airboss stations
* **[Autoplanner]** Plan Air-to-Air Escorts for AWACS & Tankers
## Fixes
* **[UI/UX]** A-10A flights can be edited again

View File

@ -1,6 +1,7 @@
from dataclasses import dataclass, field
from typing import List
from game.ato import FlightType
from game.ato.package import Package
@ -11,6 +12,16 @@ class AirTaskingOrder:
#: The set of all planned packages in the ATO.
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:
"""Adds a package to the ATO."""
self.packages.append(package)

View File

@ -11,6 +11,7 @@ from .formationattack import (
)
from .waypointbuilder import WaypointBuilder
from .. import FlightType
from ..packagewaypoints import PackageWaypoints
from ...utils import feet
@ -22,16 +23,28 @@ class EscortFlightPlan(FormationAttackFlightPlan):
class Builder(FormationAttackBuilder[EscortFlightPlan, 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)
ingress, target = builder.escort(
self.package.waypoints.ingress, self.package.target
)
if non_formation_escort:
target.position = self.package.waypoints.join
ingress.only_for_player = True
target.only_for_player = True
hold = None
if not self.flight.is_helo:
if not (self.flight.is_helo or non_formation_escort):
hold = builder.hold(self._hold_point())
join_pos = (

View File

@ -4,6 +4,7 @@ from typing import Optional, TYPE_CHECKING
from game.theater import ControlPoint, MissionTarget, OffMapSpawn
from game.utils import nautical_miles
from ..ato import FlightType
from ..ato.flight import Flight
from ..ato.package import Package
from ..ato.starttype import StartType
@ -46,10 +47,18 @@ class PackageBuilder:
caller should return any previously planned flights to the inventory
using release_planned_aircraft.
"""
target = self.package.target
heli = False
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(
self.package.target,
target,
plan.task,
plan.num_aircraft,
heli,

View File

@ -83,12 +83,19 @@ class PackageFulfiller:
purchase_multiplier: int,
ignore_range: bool = False,
) -> None:
if not builder.plan_flight(flight, ignore_range):
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):
heli = pf.is_helo if pf else False
missing_types.add(flight.task)
purchase_order = AircraftProcurementRequest(
near=mission.location,
near=target,
task_capability=flight.task,
number=flight.num_aircraft * purchase_multiplier,
heli=heli,

View File

@ -102,8 +102,14 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
state.context.settings,
)
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(
ProposedMission(self.target, self.flights),
ProposedMission(self.target, self.flights, asap=asap),
self.purchase_multiplier,
state.context.now,
state.context.tracer,

View File

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

View File

@ -25,3 +25,4 @@ class PlanRefueling(PackagePlanningTask[MissionTarget]):
def propose_flights(self) -> None:
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 typing import Any
import numpy as np
from dcs.mapping import heading_between_points
from shapely.geometry import Point, MultiPolygon, Polygon
from shapely.geometry.base import BaseGeometry as Geometry, BaseGeometry
@ -232,6 +233,7 @@ class WaypointStrategy:
min_distance_from_threat_to_target_buffer = target.buffer(
target_size.meters
).distance(self.threat_zones.boundary)
if np.isfinite(min_distance_from_threat_to_target_buffer).all():
threat_mask = self.threat_zones.buffer(
-min_distance_from_threat_to_target_buffer - wiggle.meters
)

View File

@ -689,6 +689,16 @@ class Settings:
max=100,
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
# Gameplay

View File

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