mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Recovery tanker support (#429)
* fix conflict * squash bugs and reuse patrol layout * fix tanker tacan and formatting * fix unlimited fuel option * update pretense for tanker changes * reuse refueling flight plan and bugfix for sunken carrier changelog * remove unitmap dependency * formatting and more unit map removal * more formatting * typing and black * keep tanker out of clouds * fix if there are no clouds * better cloud handling * groundwork for recovery task * remove changes to game/commander * Finishing up recovery tankers --------- Co-authored-by: Raffson <Raffson@users.noreply.github.com>
This commit is contained in:
parent
a4671571bc
commit
dd7e4c908e
@ -41,6 +41,7 @@
|
||||
* **[Mission Generation]** Enable Supercarrier's LSO & Airboss stations
|
||||
* **[UX]** Default settings are now loaded from Default.zip
|
||||
* **[Autoplanner]** Plan Air-to-Air Escorts for AWACS & Tankers
|
||||
* **[Package Planning]** Ability to plan recovery tanker flights
|
||||
|
||||
## Fixes
|
||||
* **[UI/UX]** A-10A flights can be edited again
|
||||
|
||||
@ -60,6 +60,16 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def landing_time(self) -> datetime:
|
||||
arrival = (
|
||||
self.layout.custom_waypoints[-1]
|
||||
if self.layout.custom_waypoints
|
||||
else self.layout.departure
|
||||
)
|
||||
return_time = self.total_time_between_waypoints(self.tot_waypoint, arrival)
|
||||
return self.tot + return_time
|
||||
|
||||
|
||||
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
|
||||
def __init__(
|
||||
|
||||
@ -296,6 +296,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
"""The time that the mission is complete and the flight RTBs."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def landing_time(self) -> datetime:
|
||||
raise NotImplementedError
|
||||
|
||||
@self_type_guard
|
||||
def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]:
|
||||
return False
|
||||
|
||||
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from typing import Any, TYPE_CHECKING, Type
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.theater.controlpoint import NavalControlPoint
|
||||
from .aewc import AewcFlightPlan
|
||||
from .airassault import AirAssaultFlightPlan
|
||||
from .airlift import AirliftFlightPlan
|
||||
@ -22,6 +23,7 @@ from .planningerror import PlanningError
|
||||
from .pretensecargo import PretenseCargoFlightPlan
|
||||
from .sead import SeadFlightPlan
|
||||
from .seadsweep import SeadSweepFlightPlan
|
||||
from .shiprecoverytanker import RecoveryTankerFlightPlan
|
||||
from .strike import StrikeFlightPlan
|
||||
from .sweep import SweepFlightPlan
|
||||
from .tarcap import TarCapFlightPlan
|
||||
@ -64,6 +66,7 @@ class FlightPlanBuilderTypes:
|
||||
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
||||
FlightType.PRETENSE_CARGO: PretenseCargoFlightPlan.builder_type(),
|
||||
FlightType.ARMED_RECON: ArmedReconFlightPlan.builder_type(),
|
||||
FlightType.RECOVERY: RecoveryTankerFlightPlan.builder_type(),
|
||||
}
|
||||
try:
|
||||
return builder_dict[flight.flight_type]
|
||||
|
||||
61
game/ato/flightplans/shiprecoverytanker.py
Normal file
61
game/ato/flightplans/shiprecoverytanker.py
Normal file
@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Type
|
||||
|
||||
from game.ato.flightplans.ibuilder import IBuilder
|
||||
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||
from .patrolling import PatrollingLayout
|
||||
from .refuelingflightplan import RefuelingFlightPlan
|
||||
from .. import FlightWaypoint
|
||||
from ...utils import knots
|
||||
|
||||
|
||||
class RecoveryTankerFlightPlan(RefuelingFlightPlan):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return self.flight.coalition.game.settings.desired_tanker_on_station_time
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.departure
|
||||
|
||||
|
||||
class Builder(IBuilder[RecoveryTankerFlightPlan, PatrollingLayout]):
|
||||
def layout(self) -> PatrollingLayout:
|
||||
|
||||
builder = WaypointBuilder(self.flight)
|
||||
altitude = builder.get_patrol_altitude
|
||||
|
||||
station_time = self.coalition.game.settings.desired_tanker_on_station_time
|
||||
time_to_landing = station_time.total_seconds()
|
||||
hdg = (self.coalition.game.conditions.weather.wind.at_0m.direction + 180) % 360
|
||||
recovery_ship = self.package.target.position.point_from_heading(
|
||||
hdg, time_to_landing * knots(20).meters_per_second
|
||||
)
|
||||
recovery_tanker = builder.recovery_tanker(recovery_ship)
|
||||
patrol_end = builder.race_track_end(recovery_tanker.position, altitude)
|
||||
patrol_end.only_for_player = True # avoid generating the waypoints
|
||||
|
||||
return PatrollingLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position, recovery_ship, altitude
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
recovery_ship, self.flight.arrival.position, altitude
|
||||
),
|
||||
patrol_start=recovery_tanker,
|
||||
patrol_end=patrol_end,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
custom_waypoints=list(),
|
||||
)
|
||||
|
||||
def build(self, dump_debug_info: bool = False) -> RecoveryTankerFlightPlan:
|
||||
return RecoveryTankerFlightPlan(self.flight, self.layout())
|
||||
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from abc import ABC
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, TypeVar, Optional
|
||||
|
||||
from game.ato.flightplans.flightplan import FlightPlan, Layout
|
||||
@ -86,3 +87,10 @@ class StandardFlightPlan(FlightPlan[LayoutT], ABC):
|
||||
others are guaranteed to have certain properties like departure and arrival points,
|
||||
potentially a divert field, and a bullseye
|
||||
"""
|
||||
|
||||
@property
|
||||
def landing_time(self) -> datetime:
|
||||
return_time = self.total_time_between_waypoints(
|
||||
self.tot_waypoint, self.layout.arrival
|
||||
)
|
||||
return self.tot + return_time
|
||||
|
||||
@ -800,3 +800,18 @@ class WaypointBuilder:
|
||||
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
return point + Vector2(x_adj, y_adj)
|
||||
|
||||
@staticmethod
|
||||
def recovery_tanker(position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"RECOVERY",
|
||||
FlightWaypointType.RECOVERY_TANKER,
|
||||
position,
|
||||
feet(6000),
|
||||
alt_type,
|
||||
description="Recovery tanker for aircraft carriers",
|
||||
pretty_name="Recovery",
|
||||
only_for_player=True, # for visual purposes in Retribution only
|
||||
)
|
||||
|
||||
@ -60,6 +60,7 @@ class FlightType(Enum):
|
||||
SEAD_SWEEP = "SEAD Sweep" # Reintroduce legacy "engage-whatever-you-can-find" SEAD
|
||||
PRETENSE_CARGO = "Cargo Transport" # For Pretense campaign AI cargo planes
|
||||
ARMED_RECON = "Armed Recon"
|
||||
RECOVERY = "Recovery"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@ -117,6 +118,7 @@ class FlightType(Enum):
|
||||
FlightType.INTERCEPTION: AirEntity.FIGHTER,
|
||||
FlightType.OCA_AIRCRAFT: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.OCA_RUNWAY: AirEntity.ATTACK_STRIKE,
|
||||
FlightType.RECOVERY: AirEntity.TANKER,
|
||||
FlightType.REFUELING: AirEntity.TANKER,
|
||||
FlightType.SEAD: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,
|
||||
FlightType.SEAD_ESCORT: AirEntity.SUPPRESSION_OF_ENEMY_AIR_DEFENCE,
|
||||
|
||||
@ -52,3 +52,4 @@ class FlightWaypointType(IntEnum):
|
||||
INGRESS_ANTI_SHIP = 32
|
||||
INGRESS_SEAD_SWEEP = 33
|
||||
INGRESS_ARMED_RECON = 34
|
||||
RECOVERY_TANKER = 35 # Tanker recovery point
|
||||
|
||||
@ -188,6 +188,7 @@ class Package(RadioFrequencyContainer):
|
||||
FlightType.BARCAP,
|
||||
FlightType.AEWC,
|
||||
FlightType.FERRY,
|
||||
FlightType.RECOVERY,
|
||||
FlightType.REFUELING,
|
||||
FlightType.SWEEP,
|
||||
FlightType.SEAD_ESCORT,
|
||||
|
||||
@ -40,6 +40,8 @@ class MissionScheduler:
|
||||
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
|
||||
]
|
||||
|
||||
carrier_etas = []
|
||||
|
||||
start_time = start_time_generator(
|
||||
count=len(non_dca_packages),
|
||||
earliest=5 * 60,
|
||||
@ -47,6 +49,8 @@ class MissionScheduler:
|
||||
margin=5 * 60,
|
||||
)
|
||||
for package in self.coalition.ato.packages:
|
||||
if package.primary_task is FlightType.RECOVERY:
|
||||
continue
|
||||
tot = TotEstimator(package).earliest_tot(now)
|
||||
if package.primary_task in dca_types:
|
||||
previous_end_time = previous_cap_end_time[package.target]
|
||||
@ -74,3 +78,19 @@ class MissionScheduler:
|
||||
# to be present. Runway and air started aircraft will be
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) + tot
|
||||
arrivals = []
|
||||
for f in package.flights:
|
||||
if f.departure.is_fleet and not f.is_helo:
|
||||
arrivals.append(f.flight_plan.landing_time - timedelta(minutes=10))
|
||||
if arrivals:
|
||||
carrier_etas.append(min(arrivals))
|
||||
|
||||
for package in [
|
||||
p
|
||||
for p in self.coalition.ato.packages
|
||||
if p.primary_task is FlightType.RECOVERY
|
||||
]:
|
||||
if carrier_etas:
|
||||
package.time_over_target = carrier_etas.pop(0)
|
||||
else:
|
||||
break
|
||||
|
||||
@ -246,6 +246,9 @@ class ObjectiveFinder:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return closest
|
||||
|
||||
def friendly_naval_control_points(self) -> Iterator[ControlPoint]:
|
||||
return (cp for cp in self.friendly_control_points() if cp.is_fleet)
|
||||
|
||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all enemy control points."""
|
||||
return (
|
||||
|
||||
@ -53,7 +53,8 @@ class PackageBuilder:
|
||||
if pf:
|
||||
target = (
|
||||
pf.departure
|
||||
if pf.flight_type in [FlightType.AEWC, FlightType.REFUELING]
|
||||
if pf.flight_type
|
||||
in [FlightType.AEWC, FlightType.REFUELING, FlightType.RECOVERY]
|
||||
else target
|
||||
)
|
||||
heli = pf.is_helo
|
||||
|
||||
@ -14,6 +14,7 @@ from game.commander.tasks.compound.interdictreinforcements import (
|
||||
InterdictReinforcements,
|
||||
)
|
||||
from game.commander.tasks.compound.protectairspace import ProtectAirSpace
|
||||
from game.commander.tasks.compound.recoverysupport import RecoverySupport
|
||||
from game.commander.tasks.compound.theatersupport import TheaterSupport
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
@ -34,3 +35,4 @@ class PlanNextAction(CompoundTask[TheaterState]):
|
||||
yield [AttackBuildings()]
|
||||
yield [AttackShips()]
|
||||
yield [DegradeIads()]
|
||||
yield [RecoverySupport()] # for recovery tankers
|
||||
|
||||
16
game/commander/tasks/compound/recoverysupport.py
Normal file
16
game/commander/tasks/compound/recoverysupport.py
Normal file
@ -0,0 +1,16 @@
|
||||
from collections.abc import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.recovery import PlanRecovery
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class RecoverySupport(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [PlanRecoverySupport()]
|
||||
|
||||
|
||||
class PlanRecoverySupport(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for target in state.recovery_targets:
|
||||
yield [PlanRecovery(target)]
|
||||
@ -16,7 +16,7 @@ from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.groups import GroupTask
|
||||
from game.settings import AutoAtoBehavior
|
||||
from game.theater import MissionTarget
|
||||
from game.theater import MissionTarget, ControlPoint
|
||||
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
||||
from game.utils import Distance, meters
|
||||
|
||||
@ -51,6 +51,15 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
return False
|
||||
return self.fulfill_mission(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
seen: set[ControlPoint] = set()
|
||||
if not self.package:
|
||||
return
|
||||
for f in self.package.flights:
|
||||
if f.departure.is_fleet and not f.is_helo and f.departure not in seen:
|
||||
state.recovery_targets[f.departure] += f.count
|
||||
seen.add(f.departure)
|
||||
|
||||
def execute(self, coalition: Coalition) -> None:
|
||||
if self.package is None:
|
||||
raise RuntimeError("Attempted to execute failed package planning task")
|
||||
|
||||
@ -22,6 +22,7 @@ class PlanAewc(PackagePlanningTask[MissionTarget]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.aewc_targets.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.AEWC, 1)
|
||||
|
||||
@ -19,6 +19,7 @@ class PlanAirAssault(PackagePlanningTask[ControlPoint]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.vulnerable_control_points.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.AIR_ASSAULT, self.get_flight_size())
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.missionproposals import EscortType
|
||||
@ -21,6 +20,7 @@ class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.eliminate_ship(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
size = self.get_flight_size()
|
||||
|
||||
@ -20,6 +20,7 @@ class PlanAntiShipping(PackagePlanningTask[CargoShip]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.enemy_shipping.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
size = self.get_flight_size()
|
||||
|
||||
@ -19,6 +19,7 @@ class PlanArmedRecon(PackagePlanningTask[ControlPoint]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.control_point_priority_queue.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.ARMED_RECON, self.get_flight_size())
|
||||
|
||||
@ -19,6 +19,7 @@ class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.eliminate_battle_position(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
tgt_count = self.target.alive_unit_count
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
@ -21,6 +19,7 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.barcaps_needed[self.target] -= 1
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
size = self.get_flight_size()
|
||||
|
||||
@ -28,6 +28,7 @@ class PlanCas(PackagePlanningTask[FrontLine]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.vulnerable_front_lines.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
size = self.get_flight_size()
|
||||
|
||||
@ -19,6 +19,7 @@ class PlanConvoyInterdiction(PackagePlanningTask[Convoy]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.enemy_convoys.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.BAI, 2)
|
||||
|
||||
@ -23,6 +23,7 @@ class PlanDead(PackagePlanningTask[IadsGroundObject]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.eliminate_air_defense(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
tgt_count = self.target.alive_unit_count
|
||||
|
||||
@ -22,6 +22,7 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.oca_targets.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
size = self.get_flight_size()
|
||||
|
||||
32
game/commander/tasks/primitive/recovery.py
Normal file
32
game/commander/tasks/primitive/recovery.py
Normal file
@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanRecovery(PackagePlanningTask[ControlPoint]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if (
|
||||
state.context.coalition.player
|
||||
and not state.context.settings.auto_ato_behavior_tankers
|
||||
):
|
||||
return False
|
||||
ac_per_tanker = state.context.settings.aircraft_per_recovery_tanker
|
||||
if not (
|
||||
self.target in state.recovery_targets
|
||||
and state.recovery_targets[self.target] >= ac_per_tanker
|
||||
):
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
ac_per_tanker = state.context.settings.aircraft_per_recovery_tanker
|
||||
state.recovery_targets[self.target] -= ac_per_tanker
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.RECOVERY, 1)
|
||||
@ -22,6 +22,7 @@ class PlanRefueling(PackagePlanningTask[MissionTarget]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.refueling_targets.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.REFUELING, 1)
|
||||
|
||||
@ -20,6 +20,7 @@ class PlanStrike(PackagePlanningTask[TheaterGroundObject]):
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.strike_targets.remove(self.target)
|
||||
super().apply_effects(state)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
tgt_count = self.target.alive_unit_count
|
||||
|
||||
@ -15,7 +15,12 @@ from game.ground_forces.combat_stance import CombatStance
|
||||
from game.htn import WorldState
|
||||
from game.profiling import MultiEventTracer
|
||||
from game.settings import Settings
|
||||
from game.theater import ConflictTheater, ControlPoint, FrontLine, MissionTarget
|
||||
from game.theater import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
FrontLine,
|
||||
MissionTarget,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
IadsGroundObject,
|
||||
@ -51,6 +56,7 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
vulnerable_front_lines: list[FrontLine]
|
||||
aewc_targets: list[MissionTarget]
|
||||
refueling_targets: list[MissionTarget]
|
||||
recovery_targets: dict[ControlPoint, int]
|
||||
enemy_air_defenses: list[IadsGroundObject]
|
||||
threatening_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]]
|
||||
detecting_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]]
|
||||
@ -118,6 +124,7 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
vulnerable_front_lines=list(self.vulnerable_front_lines),
|
||||
aewc_targets=list(self.aewc_targets),
|
||||
refueling_targets=list(self.refueling_targets),
|
||||
recovery_targets=dict(self.recovery_targets),
|
||||
enemy_air_defenses=list(self.enemy_air_defenses),
|
||||
enemy_convoys=list(self.enemy_convoys),
|
||||
enemy_shipping=list(self.enemy_shipping),
|
||||
@ -186,6 +193,7 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
vulnerable_front_lines=list(finder.front_lines()),
|
||||
aewc_targets=[finder.farthest_friendly_control_point()],
|
||||
refueling_targets=[finder.closest_friendly_control_point()],
|
||||
recovery_targets={cp: 0 for cp in finder.friendly_naval_control_points()},
|
||||
enemy_air_defenses=list(finder.enemy_air_defenses()),
|
||||
threatening_air_defenses=[],
|
||||
detecting_air_defenses=[],
|
||||
|
||||
@ -262,6 +262,12 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
):
|
||||
enrich[FlightType.ARMED_RECON] = value
|
||||
|
||||
if FlightType.RECOVERY not in self.task_priorities:
|
||||
if (
|
||||
value := self.task_priorities.get(FlightType.REFUELING)
|
||||
) and self.carrier_capable is True:
|
||||
enrich[FlightType.RECOVERY] = value
|
||||
|
||||
self.task_priorities.update(enrich)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
|
||||
@ -554,6 +554,7 @@ class Game:
|
||||
FlightType.TRANSPORT,
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
FlightType.RECOVERY,
|
||||
]:
|
||||
# BARCAPs will be planned at most locations on smaller theaters,
|
||||
# rendering culling fairly useless. BARCAP packages don't really
|
||||
|
||||
@ -30,22 +30,33 @@ from dcs.task import (
|
||||
OptNoReportWaypointPass,
|
||||
OptRadioUsageContact,
|
||||
OptRadioSilence,
|
||||
Tanker,
|
||||
RecoveryTanker,
|
||||
ActivateBeaconCommand,
|
||||
ControlledTask,
|
||||
)
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup
|
||||
|
||||
from game.ato import Flight, FlightType
|
||||
from game.ato import Flight, FlightType, Package
|
||||
from game.ato.flightplans.aewc import AewcFlightPlan
|
||||
from game.ato.flightplans.formationattack import FormationAttackLayout
|
||||
from game.ato.flightplans.packagerefueling import PackageRefuelingFlightPlan
|
||||
from game.ato.flightplans.shiprecoverytanker import RecoveryTankerFlightPlan
|
||||
from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
|
||||
from game.utils import nautical_miles
|
||||
from game.missiongenerator.missiondata import MissionData
|
||||
from game.utils import nautical_miles, knots, feet
|
||||
|
||||
|
||||
class AircraftBehavior:
|
||||
def __init__(self, task: FlightType) -> None:
|
||||
def __init__(self, task: FlightType, mission_data: MissionData) -> None:
|
||||
self.task = task
|
||||
self.mission_data = mission_data
|
||||
|
||||
def apply_to(self, flight: Flight, group: FlyingGroup[Any]) -> None:
|
||||
def apply_to(
|
||||
self,
|
||||
flight: Flight,
|
||||
group: FlyingGroup[Any],
|
||||
) -> None:
|
||||
if self.task in [
|
||||
FlightType.BARCAP,
|
||||
FlightType.TARCAP,
|
||||
@ -58,6 +69,8 @@ class AircraftBehavior:
|
||||
self.configure_awacs(group, flight)
|
||||
elif self.task == FlightType.REFUELING:
|
||||
self.configure_refueling(group, flight)
|
||||
elif self.task == FlightType.RECOVERY:
|
||||
self.configure_recovery(group, flight)
|
||||
elif self.task in [FlightType.CAS, FlightType.BAI]:
|
||||
self.configure_cas(group, flight)
|
||||
elif self.task == FlightType.ARMED_RECON:
|
||||
@ -317,6 +330,7 @@ class AircraftBehavior:
|
||||
if not (
|
||||
isinstance(flight.flight_plan, TheaterRefuelingFlightPlan)
|
||||
or isinstance(flight.flight_plan, PackageRefuelingFlightPlan)
|
||||
or isinstance(flight.flight_plan, RecoveryTankerFlightPlan)
|
||||
):
|
||||
logging.error(
|
||||
f"Cannot configure racetrack refueling tasks for {flight} because it "
|
||||
@ -332,6 +346,95 @@ class AircraftBehavior:
|
||||
restrict_jettison=True,
|
||||
)
|
||||
|
||||
def configure_recovery(
|
||||
self,
|
||||
group: FlyingGroup[Any],
|
||||
flight: Flight,
|
||||
) -> None:
|
||||
self.configure_refueling(group, flight)
|
||||
if not isinstance(flight.flight_plan, RecoveryTankerFlightPlan):
|
||||
logging.error(
|
||||
f"Cannot configure recovery task for {flight} because it "
|
||||
"does not have an recovery tanker flight plan."
|
||||
)
|
||||
return
|
||||
|
||||
self.configure_tanker_tacan(flight, group)
|
||||
|
||||
clouds = flight.squadron.coalition.game.conditions.weather.clouds
|
||||
speed = knots(250).meters_per_second
|
||||
altitude = feet(6000).meters
|
||||
if clouds is not None:
|
||||
if abs(clouds.base - altitude) < feet(1000).meters:
|
||||
altitude = clouds.base - feet(1000).meters
|
||||
if altitude < feet(2000).meters:
|
||||
altitude = clouds.base + feet(6000).meters
|
||||
|
||||
naval_group = self._get_carrier_group(flight.package)
|
||||
last_waypoint = len(naval_group.points) # last waypoint of the CVN/LHA
|
||||
|
||||
tanker_tos = flight.coalition.game.settings.desired_tanker_on_station_time
|
||||
lua_predicate = f"""
|
||||
local lowfuel = false
|
||||
for i, unitObject in pairs(Group.getByName('{group.name}'):getUnits()) do
|
||||
if Unit.getFuel(unitObject) < 0.2 then lowfuel = true end
|
||||
end
|
||||
return lowfuel
|
||||
"""
|
||||
|
||||
tanker = ControlledTask(Tanker())
|
||||
tanker.stop_after_duration(int(tanker_tos.total_seconds()) + 1)
|
||||
tanker.stop_if_lua_predicate(lua_predicate)
|
||||
group.points[0].add_task(tanker)
|
||||
|
||||
recovery = ControlledTask(
|
||||
RecoveryTanker(naval_group.id, speed, altitude, last_waypoint)
|
||||
)
|
||||
recovery.stop_if_lua_predicate(lua_predicate)
|
||||
recovery.stop_after_duration(int(tanker_tos.total_seconds()) + 1)
|
||||
group.points[0].add_task(recovery)
|
||||
|
||||
def configure_tanker_tacan(self, flight: Flight, group: FlyingGroup[Any]) -> None:
|
||||
tanker_info = self.mission_data.tankers[-1]
|
||||
tacan = tanker_info.tacan
|
||||
if flight.unit_type.dcs_unit_type.tacan and tacan:
|
||||
if flight.tcn_name is None:
|
||||
cs = tanker_info.callsign[:-2]
|
||||
csn = tanker_info.callsign[-1]
|
||||
tacan_callsign = {
|
||||
"Texaco": "TX",
|
||||
"Arco": "AC",
|
||||
"Shell": "SH",
|
||||
}.get(cs)
|
||||
if tacan_callsign:
|
||||
tacan_callsign = tacan_callsign + csn
|
||||
else:
|
||||
tacan_callsign = cs[0:2] + csn
|
||||
else:
|
||||
tacan_callsign = flight.tcn_name
|
||||
|
||||
group.points[0].add_task(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign.upper(),
|
||||
bearing=True,
|
||||
unit_id=group.units[0].id,
|
||||
aa=True,
|
||||
)
|
||||
)
|
||||
|
||||
def _get_carrier_group(self, package: Package) -> ShipGroup:
|
||||
name = package.target.name
|
||||
carrier_position = package.target.position
|
||||
for carrier in self.mission_data.carriers:
|
||||
if carrier.ship_group.position == carrier_position:
|
||||
return carrier.ship_group
|
||||
raise RuntimeError(
|
||||
f"Could not find a carrier in the mission matching {name} at "
|
||||
f"({carrier_position.x}, {carrier_position.y})"
|
||||
)
|
||||
|
||||
def configure_escort(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
self.configure_task(flight, group, Escort)
|
||||
self.configure_behavior(
|
||||
|
||||
@ -15,6 +15,7 @@ from dcs.unit import Skill
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
from game.ato import Flight, FlightType
|
||||
from game.ato.flightplans.shiprecoverytanker import RecoveryTankerFlightPlan
|
||||
from game.callsigns import callsign_for_support_unit
|
||||
from game.data.weapons import Pylon, WeaponType
|
||||
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
|
||||
@ -79,12 +80,14 @@ class FlightGroupConfigurator:
|
||||
self.use_client = use_client
|
||||
|
||||
def configure(self) -> FlightData:
|
||||
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
|
||||
flight_channel = self.setup_radios()
|
||||
AircraftBehavior(self.flight.flight_type, self.mission_data).apply_to(
|
||||
self.flight, self.group
|
||||
)
|
||||
AircraftPainter(self.flight, self.group).apply_livery()
|
||||
self.setup_props()
|
||||
self.setup_payloads()
|
||||
self.setup_fuel()
|
||||
flight_channel = self.setup_radios()
|
||||
|
||||
laser_codes: list[Optional[int]] = []
|
||||
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
||||
@ -197,7 +200,11 @@ class FlightGroupConfigurator:
|
||||
if freq not in self.radio_registry.allocated_channels:
|
||||
self.radio_registry.reserve(freq)
|
||||
|
||||
if self.flight.flight_type in {FlightType.AEWC, FlightType.REFUELING}:
|
||||
if self.flight.flight_type in {
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
FlightType.RECOVERY,
|
||||
}:
|
||||
self.register_air_support(freq)
|
||||
elif self.flight.client_count and self.flight.squadron.radio_presets:
|
||||
freq = self.flight.squadron.radio_presets["intra_flight"][0]
|
||||
@ -222,9 +229,11 @@ class FlightGroupConfigurator:
|
||||
unit=self.group.units[0],
|
||||
)
|
||||
)
|
||||
elif isinstance(
|
||||
self.flight.flight_plan, TheaterRefuelingFlightPlan
|
||||
) or isinstance(self.flight.flight_plan, PackageRefuelingFlightPlan):
|
||||
elif (
|
||||
isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan)
|
||||
or isinstance(self.flight.flight_plan, PackageRefuelingFlightPlan)
|
||||
or isinstance(self.flight.flight_plan, RecoveryTankerFlightPlan)
|
||||
):
|
||||
tacan = self.flight.tacan
|
||||
if tacan is None and self.flight.squadron.aircraft.dcs_unit_type.tacan:
|
||||
try:
|
||||
|
||||
@ -100,7 +100,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
tacan_callsign.upper(),
|
||||
bearing=True,
|
||||
unit_id=self.group.units[0].id,
|
||||
aa=True,
|
||||
|
||||
@ -9,8 +9,6 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
class RaceTrackEndBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
flight_plan = self.flight.flight_plan
|
||||
|
||||
# Unlimited fuel option : enable at racetrack end. Must be first option to work.
|
||||
if self.flight.squadron.coalition.game.settings.ai_unlimited_fuel:
|
||||
waypoint.tasks.insert(0, SetUnlimitedFuelCommand(True))
|
||||
|
||||
@ -8,6 +8,17 @@ class RefuelPointBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
if not self.ai_despawn(waypoint, True):
|
||||
refuel = ControlledTask(RefuelingTaskAction())
|
||||
refuel.start_probability(10)
|
||||
refuel.start_if_lua_predicate(self._get_lua_predicate(0.2))
|
||||
refuel.stop_if_lua_predicate(self._get_lua_predicate(0.5))
|
||||
waypoint.add_task(refuel)
|
||||
return super().add_tasks(waypoint)
|
||||
|
||||
def _get_lua_predicate(self, fuel_level: float) -> str:
|
||||
return f"""
|
||||
local okfuel = true
|
||||
for i, unitObject in pairs(Group.getByName('{self.group.name}'):getUnits()) do
|
||||
--trigger.action.outText(tostring(Unit.getFuel(unitObject)), 15)
|
||||
if Unit.getFuel(unitObject) < {fuel_level} then okfuel = false; break end
|
||||
end
|
||||
return lowfuel
|
||||
"""
|
||||
|
||||
@ -5,6 +5,7 @@ from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.flyingunit import FlyingUnit
|
||||
from dcs.unitgroup import ShipGroup
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
@ -58,6 +59,7 @@ class CarrierInfo(UnitInfo):
|
||||
tacan: TacanChannel
|
||||
icls_channel: int | None
|
||||
link4_freq: RadioFrequency | None
|
||||
ship_group: ShipGroup
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@ -650,6 +650,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
icls_channel=icls,
|
||||
link4_freq=link4,
|
||||
blue=self.control_point.captured,
|
||||
ship_group=ship_group,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -77,12 +77,14 @@ class PretenseFlightGroupConfigurator(FlightGroupConfigurator):
|
||||
self.use_client = use_client
|
||||
|
||||
def configure(self) -> FlightData:
|
||||
AircraftBehavior(self.flight.flight_type).apply_to(self.flight, self.group)
|
||||
flight_channel = self.setup_radios()
|
||||
AircraftBehavior(self.flight.flight_type, self.mission_data).apply_to(
|
||||
self.flight, self.group
|
||||
)
|
||||
AircraftPainter(self.flight, self.group).apply_livery()
|
||||
self.setup_props()
|
||||
self.setup_payloads()
|
||||
self.setup_fuel()
|
||||
flight_channel = self.setup_radios()
|
||||
|
||||
laser_codes: list[Optional[int]] = []
|
||||
for unit, pilot in zip(self.group.units, self.flight.roster.members):
|
||||
|
||||
@ -744,6 +744,7 @@ class PretenseGenericCarrierGenerator(GenericCarrierGenerator):
|
||||
icls_channel=icls,
|
||||
link4_freq=link4,
|
||||
blue=self.control_point.captured,
|
||||
ship_group=ship_group,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -247,6 +247,17 @@ class Settings:
|
||||
"provided the faction has access to them."
|
||||
),
|
||||
)
|
||||
aircraft_per_recovery_tanker: int = bounded_int_option(
|
||||
"Number of aircraft per recovery tanker",
|
||||
page=CAMPAIGN_DOCTRINE_PAGE,
|
||||
section=GENERAL_SECTION,
|
||||
default=4,
|
||||
min=2,
|
||||
max=12,
|
||||
detail=(
|
||||
"A higher number will force the autoplanner to generate less recovery tankers."
|
||||
),
|
||||
)
|
||||
oca_target_autoplanner_min_aircraft_count: int = bounded_int_option(
|
||||
"Minimum number of aircraft (at vulnerable airfields) for auto-planner to plan OCA packages against",
|
||||
page=CAMPAIGN_DOCTRINE_PAGE,
|
||||
|
||||
@ -1369,6 +1369,9 @@ class NavalControlPoint(
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
FlightType.RECOVERY,
|
||||
FlightType.REFUELING,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: Buddy tanking for the A-4?
|
||||
# TODO: Rescue chopper?
|
||||
@ -1382,8 +1385,7 @@ class NavalControlPoint(
|
||||
yield from super().mission_types(for_player)
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
# Nothing yet
|
||||
]
|
||||
|
||||
@property
|
||||
|
||||
@ -159,6 +159,21 @@ class QAutoCreateDialog(QDialog):
|
||||
self.refueling_type,
|
||||
)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
self.recovery = self._create_checkbox("Recovery")
|
||||
self.recovery_count = _spinbox_template()
|
||||
self.recovery_count.setValue(1)
|
||||
hbox.addWidget(self.recovery)
|
||||
hbox.addWidget(self.recovery_count)
|
||||
self.recovery_type = self._create_type_selector(FlightType.RECOVERY)
|
||||
hbox.addWidget(self.recovery_type, 1)
|
||||
self.layout.addLayout(hbox)
|
||||
self.checkboxes[self.recovery] = (
|
||||
FlightType.RECOVERY,
|
||||
self.recovery_count,
|
||||
self.recovery_type,
|
||||
)
|
||||
|
||||
self.create_button = QPushButton("Create")
|
||||
self.create_button.setProperty("style", "start-button")
|
||||
self.create_button.clicked.connect(self.on_create_clicked)
|
||||
|
||||
@ -26,3 +26,4 @@ variants:
|
||||
S-3B Tanker: {}
|
||||
tasks:
|
||||
Refueling: 0
|
||||
Recovery: 0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user