mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Autoplan Air-to-Air Escorts for AWACS & Tankers
This commit is contained in:
parent
d2fa027cdd
commit
e02698d8a8
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 = (
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -83,12 +83,19 @@ class PackageFulfiller:
|
|||||||
purchase_multiplier: int,
|
purchase_multiplier: int,
|
||||||
ignore_range: bool = False,
|
ignore_range: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not builder.plan_flight(flight, ignore_range):
|
target = mission.location
|
||||||
pf = builder.package.primary_flight
|
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
|
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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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,6 +233,7 @@ 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)
|
||||||
|
if np.isfinite(min_distance_from_threat_to_target_buffer).all():
|
||||||
threat_mask = self.threat_zones.buffer(
|
threat_mask = self.threat_zones.buffer(
|
||||||
-min_distance_from_threat_to_target_buffer - wiggle.meters
|
-min_distance_from_threat_to_target_buffer - wiggle.meters
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user