mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Add plannable tankers.
This Pull Request lets users plan Tanker flights. Features: - Introduction of `Refueling` flight type. - Tankers can be purchased at airbases and carriers. - Tankers get planned by AI. - Tankers are planned from airbases and at aircraft carriers. - Tankers aim to be at high, fast, and 70 miles from the nearest threat. (A10s won't be able to tank) - Tankers racetrack orbit for one hour. - Optional Tickbox to enable legacy tankers. - S-3B Tanker added to factions. - KC-130 MPRS added to factions. - Kneeboard shows planned tankers, their tacans, and radios. Limitations: - AI doesn't know whether to plan probe and drogue or boom refueling tankers. - User can't choose tanker speed. Heavily loaded aircraft may have trouble. - User can't choose tanker altitude. A-10s will not make it to high altitude. Problems: - Tanker callsigns do not increment, see attached image. (Investigated: Need to use `FlyingType.callsign_dict`, instead of just `FlyingType.callsign`. This seems like it might be significant work to do.). - Having a flight of two or more tankers only spawns one tanker. - Let me know if you have a solution, or feel free to commit one. https://user-images.githubusercontent.com/74509817/120909602-d7bc3680-c633-11eb-80d7-eccd4e095770.png
This commit is contained in:
parent
a9dacf4a29
commit
a53a648a63
@ -4,6 +4,10 @@ Saves from 3.x are not compatible with 4.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Flight Planner]** Added ability to plan Tankers.
|
||||
* **[Campaign AI]** AI will plan Tanker flights.
|
||||
* **[Factions]** Added more tankers to factions.
|
||||
|
||||
## Fixes
|
||||
|
||||
# 3.0.0
|
||||
|
||||
@ -798,6 +798,7 @@ CARRIER_CAPABLE = [
|
||||
Su_33,
|
||||
A_4E_C,
|
||||
S_3B,
|
||||
S_3B_Tanker,
|
||||
E_2C,
|
||||
UH_1H,
|
||||
Mi_8MT,
|
||||
|
||||
@ -167,7 +167,9 @@ class Faction:
|
||||
faction.awacs = load_all_aircraft(json.get("awacs", []))
|
||||
faction.tankers = load_all_aircraft(json.get("tankers", []))
|
||||
|
||||
faction.aircrafts = list(set(faction.aircrafts + faction.awacs))
|
||||
faction.aircrafts = list(
|
||||
set(faction.aircrafts + faction.awacs + faction.tankers)
|
||||
)
|
||||
|
||||
faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
|
||||
faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
|
||||
|
||||
@ -375,6 +375,7 @@ class Operation:
|
||||
cls.game.settings,
|
||||
cls.game,
|
||||
cls.radio_registry,
|
||||
cls.tacan_registry,
|
||||
cls.unit_map,
|
||||
air_support=cls.airsupportgen.air_support,
|
||||
)
|
||||
|
||||
@ -44,6 +44,7 @@ class Settings:
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
restrict_weapons_by_date: bool = False
|
||||
disable_legacy_aewc: bool = True
|
||||
disable_legacy_tanker: bool = True
|
||||
generate_dark_kneeboard: bool = False
|
||||
invulnerable_player_pilots: bool = True
|
||||
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
|
||||
|
||||
@ -810,6 +810,7 @@ class Airfield(ControlPoint):
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
@ -959,7 +960,10 @@ class Carrier(NavalControlPoint):
|
||||
|
||||
yield from super().mission_types(for_player)
|
||||
if self.is_friendly(for_player):
|
||||
yield FlightType.AEWC
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
]
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("Carriers cannot be captured")
|
||||
|
||||
@ -81,6 +81,7 @@ class FrontLine(MissionTarget):
|
||||
yield from [
|
||||
FlightType.CAS,
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING
|
||||
# TODO: FlightType.TROOP_TRANSPORT
|
||||
# TODO: FlightType.EVAC
|
||||
]
|
||||
|
||||
109
gen/aircraft.py
109
gen/aircraft.py
@ -39,10 +39,12 @@ from dcs.planes import (
|
||||
Su_33,
|
||||
Tu_22M3,
|
||||
)
|
||||
from dcs.planes import IL_78M
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
AWACSTaskAction,
|
||||
ActivateBeaconCommand,
|
||||
AntishipStrike,
|
||||
AttackGroup,
|
||||
Bombing,
|
||||
@ -61,8 +63,10 @@ from dcs.task import (
|
||||
OptReactOnThreat,
|
||||
OptRestrictJettison,
|
||||
OrbitAction,
|
||||
Refueling,
|
||||
RunwayAttack,
|
||||
StartCommand,
|
||||
Tanker,
|
||||
Targets,
|
||||
Transport,
|
||||
WeaponType,
|
||||
@ -80,7 +84,7 @@ from game.data.weapons import Pylon
|
||||
from game.db import GUN_RELIANT_AIRFRAMES
|
||||
from game.factions.faction import Faction
|
||||
from game.settings import Settings
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from game.squadrons import Pilot
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@ -103,13 +107,15 @@ from gen.flights.flight import (
|
||||
)
|
||||
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
|
||||
from gen.runways import RunwayData
|
||||
from .airsupportgen import AirSupport, AwacsInfo
|
||||
from gen.tacan import TacanBand, TacanRegistry
|
||||
from .airsupportgen import AirSupport, AwacsInfo, TankerInfo
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .flights.flightplan import (
|
||||
AwacsFlightPlan,
|
||||
CasFlightPlan,
|
||||
LoiterFlightPlan,
|
||||
PatrollingFlightPlan,
|
||||
RefuelingFlightPlan,
|
||||
SweepFlightPlan,
|
||||
)
|
||||
from .flights.traveltime import GroundSpeed, TotEstimator
|
||||
@ -684,6 +690,7 @@ class AircraftConflictGenerator:
|
||||
settings: Settings,
|
||||
game: Game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
unit_map: UnitMap,
|
||||
air_support: AirSupport,
|
||||
) -> None:
|
||||
@ -691,6 +698,7 @@ class AircraftConflictGenerator:
|
||||
self.game = game
|
||||
self.settings = settings
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registy = tacan_registry
|
||||
self.unit_map = unit_map
|
||||
self.flights: List[FlightData] = []
|
||||
self.air_support = air_support
|
||||
@ -824,7 +832,10 @@ class AircraftConflictGenerator:
|
||||
OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
|
||||
)
|
||||
|
||||
if flight.flight_type == FlightType.AEWC:
|
||||
if (
|
||||
flight.flight_type == FlightType.AEWC
|
||||
or flight.flight_type == FlightType.REFUELING
|
||||
):
|
||||
channel = self.radio_registry.alloc_uhf()
|
||||
else:
|
||||
channel = self.get_intra_flight_channel(unit_type)
|
||||
@ -879,6 +890,24 @@ class AircraftConflictGenerator:
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(flight.flight_plan, RefuelingFlightPlan):
|
||||
callsign = callsign_for_support_unit(group)
|
||||
|
||||
tacan = self.tacan_registy.alloc_for_band(TacanBand.Y)
|
||||
variant = db.unit_type_name(flight.flight_plan.flight.unit_type)
|
||||
self.air_support.tankers.append(
|
||||
TankerInfo(
|
||||
group_name=str(group.name),
|
||||
callsign=callsign,
|
||||
variant=variant,
|
||||
freq=channel,
|
||||
tacan=tacan,
|
||||
start_time=flight.flight_plan.patrol_start_time,
|
||||
end_time=flight.flight_plan.patrol_end_time,
|
||||
blue=flight.departure.captured,
|
||||
)
|
||||
)
|
||||
|
||||
def _generate_at_airport(
|
||||
self,
|
||||
name: str,
|
||||
@ -1457,6 +1486,32 @@ class AircraftConflictGenerator:
|
||||
|
||||
group.points[0].tasks.append(AWACSTaskAction())
|
||||
|
||||
def configure_refueling(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
) -> None:
|
||||
group.task = Refueling.name
|
||||
|
||||
if not isinstance(flight.flight_plan, RefuelingFlightPlan):
|
||||
logging.error(
|
||||
f"Cannot configure racetrack refueling tasks for {flight} because it "
|
||||
"does not have an racetrack refueling flight plan."
|
||||
)
|
||||
return
|
||||
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.WeaponHold,
|
||||
restrict_jettison=True,
|
||||
)
|
||||
|
||||
def configure_escort(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
@ -1535,6 +1590,8 @@ class AircraftConflictGenerator:
|
||||
self.configure_sweep(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.AEWC:
|
||||
self.configure_awacs(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.REFUELING:
|
||||
self.configure_refueling(group, package, flight, dynamic_runways)
|
||||
elif flight_type in [FlightType.CAS, FlightType.BAI]:
|
||||
self.configure_cas(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.DEAD:
|
||||
@ -1602,7 +1659,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
for idx, point in enumerate(filtered_points):
|
||||
PydcsWaypointBuilder.for_waypoint(
|
||||
point, group, package, flight, self.m
|
||||
point, group, package, flight, self.m, self.air_support
|
||||
).build()
|
||||
|
||||
# Set here rather than when the FlightData is created so they waypoints
|
||||
@ -1676,12 +1733,14 @@ class PydcsWaypointBuilder:
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
mission: Mission,
|
||||
air_support: AirSupport,
|
||||
) -> None:
|
||||
self.waypoint = waypoint
|
||||
self.group = group
|
||||
self.package = package
|
||||
self.flight = flight
|
||||
self.mission = mission
|
||||
self.air_support = air_support
|
||||
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = self.group.add_waypoint(
|
||||
@ -1717,6 +1776,7 @@ class PydcsWaypointBuilder:
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
mission: Mission,
|
||||
air_support: AirSupport,
|
||||
) -> PydcsWaypointBuilder:
|
||||
builders = {
|
||||
FlightWaypointType.DROP_OFF: CargoStopBuilder,
|
||||
@ -1736,7 +1796,7 @@ class PydcsWaypointBuilder:
|
||||
FlightWaypointType.PICKUP: CargoStopBuilder,
|
||||
}
|
||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||
return builder(waypoint, group, package, flight, mission)
|
||||
return builder(waypoint, group, package, flight, mission, air_support)
|
||||
|
||||
def _viggen_client_tot(self) -> bool:
|
||||
"""Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
|
||||
@ -2119,6 +2179,8 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
# is their first priority and they will not engage any targets because
|
||||
# they're fully focused on orbiting. If the STE task is first, they will
|
||||
# engage targets if available and orbit if they find nothing to shoot.
|
||||
if self.flight.flight_type is FlightType.REFUELING:
|
||||
self.configure_refueling_actions(waypoint)
|
||||
|
||||
# TODO: Move the properties of this task into the flight plan?
|
||||
# CAP is the only current user of this so it's not a big deal, but might
|
||||
@ -2133,17 +2195,48 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
)
|
||||
)
|
||||
|
||||
racetrack = ControlledTask(
|
||||
OrbitAction(
|
||||
# TODO: Set orbit speeds for all race tracks and remove this special case.
|
||||
if isinstance(flight_plan, RefuelingFlightPlan):
|
||||
orbit = OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack,
|
||||
speed=int(flight_plan.patrol_speed.kph),
|
||||
)
|
||||
else:
|
||||
orbit = OrbitAction(
|
||||
altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
)
|
||||
)
|
||||
|
||||
racetrack = ControlledTask(orbit)
|
||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
||||
racetrack.stop_after_time(int(flight_plan.patrol_end_time.total_seconds()))
|
||||
waypoint.add_task(racetrack)
|
||||
|
||||
return waypoint
|
||||
|
||||
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
|
||||
waypoint.add_task(Tanker())
|
||||
|
||||
if self.flight.unit_type != IL_78M:
|
||||
tanker_info = self.air_support.tankers[-1]
|
||||
tacan = tanker_info.tacan
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(tanker_info.callsign)
|
||||
|
||||
waypoint.add_task(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
bearing=True,
|
||||
unit_id=self.group.units[0].id,
|
||||
aa=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class RaceTrackEndBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
|
||||
@ -54,6 +54,8 @@ class TankerInfo:
|
||||
variant: str
|
||||
freq: RadioFrequency
|
||||
tacan: TacanChannel
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
blue: bool
|
||||
|
||||
|
||||
@ -100,84 +102,86 @@ class AirSupportConflictGenerator:
|
||||
else self.conflict.red_cp
|
||||
)
|
||||
|
||||
fallback_tanker_number = 0
|
||||
if not self.game.settings.disable_legacy_tanker:
|
||||
|
||||
for i, tanker_unit_type in enumerate(
|
||||
self.game.faction_for(player=True).tankers
|
||||
):
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type)
|
||||
variant = db.unit_type_name(tanker_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||
tanker_heading = (
|
||||
self.conflict.red_cp.position.heading_between_point(
|
||||
self.conflict.blue_cp.position
|
||||
fallback_tanker_number = 0
|
||||
|
||||
for i, tanker_unit_type in enumerate(
|
||||
self.game.faction_for(player=True).tankers
|
||||
):
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type)
|
||||
variant = db.unit_type_name(tanker_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
|
||||
tanker_heading = (
|
||||
self.conflict.red_cp.position.heading_between_point(
|
||||
self.conflict.blue_cp.position
|
||||
)
|
||||
+ TANKER_HEADING_OFFSET * i
|
||||
)
|
||||
+ TANKER_HEADING_OFFSET * i
|
||||
)
|
||||
tanker_position = player_cp.position.point_from_heading(
|
||||
tanker_heading, TANKER_DISTANCE
|
||||
)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_tanker_name(
|
||||
self.mission.country(self.game.player_country), tanker_unit_type
|
||||
),
|
||||
airport=None,
|
||||
plane_type=tanker_unit_type,
|
||||
position=tanker_position,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
tanker_position = player_cp.position.point_from_heading(
|
||||
tanker_heading, TANKER_DISTANCE
|
||||
)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_tanker_name(
|
||||
self.mission.country(self.game.player_country), tanker_unit_type
|
||||
),
|
||||
airport=None,
|
||||
plane_type=tanker_unit_type,
|
||||
position=tanker_position,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
|
||||
callsign = callsign_for_support_unit(tanker_group)
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(callsign)
|
||||
if tacan_callsign is None:
|
||||
# The dict above is all the callsigns currently in the game, but
|
||||
# non-Western countries don't use the callsigns and instead just
|
||||
# use numbers. It's possible that none of those nations have
|
||||
# TACAN compatible refueling aircraft, but fallback just in
|
||||
# case.
|
||||
tacan_callsign = f"TK{fallback_tanker_number}"
|
||||
fallback_tanker_number += 1
|
||||
callsign = callsign_for_support_unit(tanker_group)
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(callsign)
|
||||
if tacan_callsign is None:
|
||||
# The dict above is all the callsigns currently in the game, but
|
||||
# non-Western countries don't use the callsigns and instead just
|
||||
# use numbers. It's possible that none of those nations have
|
||||
# TACAN compatible refueling aircraft, but fallback just in
|
||||
# case.
|
||||
tacan_callsign = f"TK{fallback_tanker_number}"
|
||||
fallback_tanker_number += 1
|
||||
|
||||
if tanker_unit_type != IL_78M:
|
||||
# Override PyDCS tacan channel.
|
||||
tanker_group.points[0].tasks.pop()
|
||||
tanker_group.points[0].tasks.append(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
True,
|
||||
tanker_group.units[0].id,
|
||||
True,
|
||||
if tanker_unit_type != IL_78M:
|
||||
# Override PyDCS tacan channel.
|
||||
tanker_group.points[0].tasks.pop()
|
||||
tanker_group.points[0].tasks.append(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
True,
|
||||
tanker_group.units[0].id,
|
||||
True,
|
||||
)
|
||||
)
|
||||
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.tankers.append(
|
||||
TankerInfo(
|
||||
str(tanker_group.name),
|
||||
callsign,
|
||||
variant,
|
||||
freq,
|
||||
tacan,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.air_support.tankers.append(
|
||||
TankerInfo(
|
||||
str(tanker_group.name),
|
||||
callsign,
|
||||
variant,
|
||||
freq,
|
||||
tacan,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
|
||||
if not self.game.settings.disable_legacy_aewc:
|
||||
possible_awacs = [
|
||||
a
|
||||
|
||||
@ -183,6 +183,7 @@ class Package:
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.AEWC,
|
||||
FlightType.REFUELING,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import operator
|
||||
import random
|
||||
from collections import defaultdict
|
||||
@ -524,6 +525,24 @@ class ObjectiveFinder:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return farthest
|
||||
|
||||
def closest_friendly_control_point(self) -> ControlPoint:
|
||||
"""Finds the friendly control point that is closest to any threats."""
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
closest = None
|
||||
min_distance = meters(math.inf)
|
||||
for cp in self.friendly_control_points():
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
continue
|
||||
distance = threat_zones.distance_to_threat(cp.position)
|
||||
if distance < min_distance:
|
||||
closest = cp
|
||||
min_distance = distance
|
||||
|
||||
if closest is None:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return closest
|
||||
|
||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all enemy control points."""
|
||||
return (
|
||||
@ -582,6 +601,7 @@ class CoalitionMissionPlanner:
|
||||
MAX_SEAD_RANGE = nautical_miles(150)
|
||||
MAX_STRIKE_RANGE = nautical_miles(150)
|
||||
MAX_AWEC_RANGE = nautical_miles(200)
|
||||
MAX_TANKER_RANGE = nautical_miles(200)
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
@ -628,6 +648,11 @@ class CoalitionMissionPlanner:
|
||||
asap=True,
|
||||
)
|
||||
|
||||
yield ProposedMission(
|
||||
self.objective_finder.closest_friendly_control_point(),
|
||||
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
|
||||
)
|
||||
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
# Plan CAP in such a way, that it is established during the whole desired mission length
|
||||
|
||||
@ -51,10 +51,13 @@ from dcs.planes import (
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
IL_76MD,
|
||||
I_16,
|
||||
IL_78M,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Ju_88A4,
|
||||
KC130,
|
||||
KC135MPRS,
|
||||
KC_135,
|
||||
KJ_2000,
|
||||
L_39ZA,
|
||||
MQ_9_Reaper,
|
||||
@ -77,6 +80,7 @@ from dcs.planes import (
|
||||
P_51D_30_NA,
|
||||
RQ_1A_Predator,
|
||||
S_3B,
|
||||
S_3B_Tanker,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_17M4,
|
||||
@ -103,8 +107,8 @@ from dcs.unittype import FlyingType
|
||||
from gen.flights.flight import FlightType
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
@ -401,6 +405,15 @@ AEWC_CAPABLE = [
|
||||
KJ_2000,
|
||||
]
|
||||
|
||||
# Priority is given to the tankers that can carry the most fuel.
|
||||
REFUELING_CAPABALE = [
|
||||
KC_135,
|
||||
KC135MPRS,
|
||||
IL_78M,
|
||||
KC130,
|
||||
S_3B_Tanker,
|
||||
]
|
||||
|
||||
|
||||
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
|
||||
@ -428,6 +441,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.AEWC:
|
||||
return AEWC_CAPABLE
|
||||
elif task == FlightType.REFUELING:
|
||||
return REFUELING_CAPABALE
|
||||
elif task == FlightType.TRANSPORT:
|
||||
return TRANSPORT_CAPABLE
|
||||
else:
|
||||
|
||||
@ -69,6 +69,7 @@ class FlightType(Enum):
|
||||
AEWC = "AEW&C"
|
||||
TRANSPORT = "Transport"
|
||||
SEAD_ESCORT = "SEAD Escort"
|
||||
REFUELING = "Refueling"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
@ -16,12 +16,21 @@ from functools import cached_property
|
||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
|
||||
from dcs.planes import (
|
||||
E_3A,
|
||||
E_2C,
|
||||
A_50,
|
||||
IL_78M,
|
||||
KC130,
|
||||
KC135MPRS,
|
||||
KC_135,
|
||||
KJ_2000,
|
||||
S_3B_Tanker,
|
||||
)
|
||||
from dcs.unit import Unit
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.squadrons import Pilot
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@ -31,7 +40,7 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.theater.theatergroundobject import EwrGroundObject
|
||||
from game.utils import Distance, Speed, feet, meters, nautical_miles
|
||||
from game.utils import Distance, Speed, feet, meters, nautical_miles, knots
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .traveltime import GroundSpeed, TravelTime
|
||||
@ -769,6 +778,28 @@ class AwacsFlightPlan(LoiterFlightPlan):
|
||||
return self.push_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RefuelingFlightPlan(PatrollingFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
bullseye: FlightWaypoint
|
||||
|
||||
#: Racetrack speed.
|
||||
patrol_speed: Speed
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
yield self.patrol_start
|
||||
yield self.patrol_end
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftFlightPlan(FlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
@ -919,6 +950,8 @@ class FlightPlanBuilder:
|
||||
return self.generate_aewc(flight)
|
||||
elif task == FlightType.TRANSPORT:
|
||||
return self.generate_transport(flight)
|
||||
elif task == FlightType.REFUELING:
|
||||
return self.generate_refueling_racetrack(flight)
|
||||
raise PlanningError(f"{task} flight plan generation not implemented")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
@ -1612,6 +1645,88 @@ class FlightPlanBuilder:
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def generate_refueling_racetrack(self, flight: Flight) -> RefuelingFlightPlan:
|
||||
location = self.package.target
|
||||
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
heading_to_threat_boundary = location.position.heading_between_point(
|
||||
closest_boundary
|
||||
)
|
||||
distance_to_threat = meters(
|
||||
location.position.distance_to_point(closest_boundary)
|
||||
)
|
||||
orbit_heading = heading_to_threat_boundary
|
||||
|
||||
# Station 70nm outside the threat zone.
|
||||
threat_buffer = nautical_miles(70)
|
||||
if self.threat_zones.threatened(location.position):
|
||||
orbit_distance = distance_to_threat + threat_buffer
|
||||
else:
|
||||
orbit_distance = distance_to_threat - threat_buffer
|
||||
|
||||
racetrack_center = location.position.point_from_heading(
|
||||
orbit_heading, orbit_distance.meters
|
||||
)
|
||||
|
||||
racetrack_half_distance = Distance.from_nautical_miles(20).meters
|
||||
|
||||
racetrack_start = racetrack_center.point_from_heading(
|
||||
orbit_heading + 90, racetrack_half_distance
|
||||
)
|
||||
racetrack_end = racetrack_center.point_from_heading(
|
||||
orbit_heading - 90, racetrack_half_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
|
||||
tanker_type = flight.unit_type
|
||||
if tanker_type is KC_135:
|
||||
# ~300 knots IAS.
|
||||
speed = knots(445)
|
||||
altitude = feet(24000)
|
||||
elif tanker_type is KC135MPRS:
|
||||
# ~300 knots IAS.
|
||||
speed = knots(440)
|
||||
altitude = feet(23000)
|
||||
elif tanker_type is KC130:
|
||||
# ~210 knots IAS, roughly the max for the KC-130 at altitude.
|
||||
speed = knots(370)
|
||||
altitude = feet(22000)
|
||||
elif tanker_type is S_3B_Tanker:
|
||||
# ~265 knots IAS.
|
||||
speed = knots(320)
|
||||
altitude = feet(12000)
|
||||
elif tanker_type is IL_78M:
|
||||
# ~280 knots IAS.
|
||||
speed = knots(400)
|
||||
altitude = feet(21000)
|
||||
else:
|
||||
# ~280 knots IAS.
|
||||
speed = knots(400)
|
||||
altitude = feet(21000)
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
return RefuelingFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
flight.departure.position, racetrack_start, altitude
|
||||
),
|
||||
nav_from=builder.nav_path(racetrack_end, flight.arrival.position, altitude),
|
||||
patrol_start=racetrack[0],
|
||||
patrol_end=racetrack[1],
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
patrol_duration=timedelta(hours=1),
|
||||
patrol_speed=speed,
|
||||
# TODO: Factor out a common base of the combat and non-combat race-tracks.
|
||||
# No harm in setting this, but we ought to clean up a bit.
|
||||
engagement_distance=meters(0),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def target_waypoint(
|
||||
flight: Flight, builder: WaypointBuilder, target: StrikeTarget
|
||||
|
||||
@ -143,4 +143,6 @@ class QUnitInfoWindow(QDialog):
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.OCA_RUNWAY}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.STRIKE}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.REFUELING_CAPABALE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.REFUELING}, "
|
||||
return aircraft_tasks[:-2]
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Optional, Set, Type
|
||||
from typing import Set, Type
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
@ -13,7 +13,6 @@ from PySide2.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
from dcs.helicopters import helicopter_map
|
||||
from dcs.task import CAP, CAS, AWACS, Transport
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
@ -45,8 +44,6 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
def init_ui(self):
|
||||
main_layout = QVBoxLayout()
|
||||
|
||||
tasks = [CAP, CAS, AWACS, Transport]
|
||||
|
||||
scroll_content = QWidget()
|
||||
task_box_layout = QGridLayout()
|
||||
row = 0
|
||||
|
||||
@ -494,6 +494,28 @@ class QSettingsWindow(QDialog):
|
||||
general_layout.addWidget(old_awac_label, 1, 0)
|
||||
general_layout.addWidget(old_awac, 1, 1, Qt.AlignRight)
|
||||
|
||||
def set_old_tanker(value: bool) -> None:
|
||||
self.game.settings.disable_legacy_tanker = not value
|
||||
|
||||
old_tanker = QCheckBox()
|
||||
old_tanker.setChecked(not self.game.settings.disable_legacy_tanker)
|
||||
old_tanker.toggled.connect(set_old_tanker)
|
||||
|
||||
old_tanker_info = (
|
||||
"If checked, an invulnerable friendly Tanker aircraft that begins the "
|
||||
"mission on station will be be spawned. This behavior will be removed in a "
|
||||
"future release."
|
||||
)
|
||||
|
||||
old_tanker.setToolTip(old_tanker_info)
|
||||
old_tanker_label = QLabel(
|
||||
"Spawn invulnerable, always-available Tanker aircraft (deprecated)."
|
||||
)
|
||||
old_tanker_label.setToolTip(old_tanker_info)
|
||||
|
||||
general_layout.addWidget(old_tanker_label, 2, 0)
|
||||
general_layout.addWidget(old_tanker, 2, 1, Qt.AlignRight)
|
||||
|
||||
campaign_layout.addWidget(HqAutomationSettingsBox(self.game))
|
||||
|
||||
def initGeneratorLayout(self):
|
||||
|
||||
@ -32,7 +32,8 @@
|
||||
],
|
||||
"tankers": [
|
||||
"KC_135",
|
||||
"KC130"
|
||||
"KC130",
|
||||
"S-3B Tanker"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M1A2_Abrams",
|
||||
|
||||
@ -33,7 +33,9 @@
|
||||
],
|
||||
"tankers": [
|
||||
"KC_135",
|
||||
"KC130"
|
||||
"KC135MPRS",
|
||||
"KC130",
|
||||
"S-3B Tanker"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M1A2_Abrams",
|
||||
|
||||
@ -28,7 +28,9 @@
|
||||
],
|
||||
"tankers": [
|
||||
"KC_135",
|
||||
"KC130"
|
||||
"KC135MPRS",
|
||||
"KC130",
|
||||
"S-3B Tanker"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M1A2_Abrams",
|
||||
|
||||
@ -15,6 +15,10 @@
|
||||
"awacs": [
|
||||
"E_2C"
|
||||
],
|
||||
"tankers": [
|
||||
"KC_135",
|
||||
"KC130"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M60A3_Patton",
|
||||
"APC_M113",
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
"awacs": [
|
||||
"E_2C"
|
||||
],
|
||||
"tankers": [
|
||||
"KC_135",
|
||||
"KC130"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M60A3_Patton",
|
||||
"APC_M113",
|
||||
|
||||
@ -32,7 +32,8 @@
|
||||
],
|
||||
"tankers": [
|
||||
"KC_135",
|
||||
"KC130"
|
||||
"KC130",
|
||||
"S-3B Tanker"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M1A2_Abrams",
|
||||
|
||||
@ -32,7 +32,9 @@
|
||||
],
|
||||
"tankers": [
|
||||
"KC_135",
|
||||
"KC130"
|
||||
"KC135MPRS",
|
||||
"KC130",
|
||||
"S-3B Tanker"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M1A2_Abrams",
|
||||
|
||||
@ -33,7 +33,9 @@
|
||||
],
|
||||
"tankers": [
|
||||
"KC_135",
|
||||
"KC130"
|
||||
"KC135MPRS",
|
||||
"KC130",
|
||||
"S-3B Tanker"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M1A2_Abrams",
|
||||
|
||||
@ -28,7 +28,9 @@
|
||||
],
|
||||
"tankers": [
|
||||
"KC_135",
|
||||
"KC130"
|
||||
"KC135MPRS",
|
||||
"KC130",
|
||||
"S-3B Tanker"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M1A2_Abrams",
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
"E_2C"
|
||||
],
|
||||
"tankers": [
|
||||
"S_3B_Tanker"
|
||||
"S-3B Tanker"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_M60A3_Patton",
|
||||
|
||||
BIN
resources/ui/units/aircrafts/icons/KC130_24.jpg
Normal file
BIN
resources/ui/units/aircrafts/icons/KC130_24.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
resources/ui/units/aircrafts/icons/KC135MPRS_24.jpg
Normal file
BIN
resources/ui/units/aircrafts/icons/KC135MPRS_24.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
resources/ui/units/aircrafts/icons/S-3B Tanker_24.jpg
Normal file
BIN
resources/ui/units/aircrafts/icons/S-3B Tanker_24.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@ -422,6 +422,36 @@
|
||||
"year-of-variant-introduction": "1995"
|
||||
}
|
||||
}],
|
||||
"KC130": [{
|
||||
"default": {
|
||||
"name": "KC-130",
|
||||
"text": "The Lockheed Martin (previously Lockheed) KC-130 is a family of the extended-range tanker version of the C-130 Hercules transport aircraft modified for aerial refueling.",
|
||||
"country-of-origin": "USA",
|
||||
"manufacturer": "Lockheed Martin",
|
||||
"role": "Tanker",
|
||||
"year-of-variant-introduction": "1962"
|
||||
}
|
||||
}],
|
||||
"KC-135": [{
|
||||
"default": {
|
||||
"name": "KC-135 Stratotanker",
|
||||
"text": "The Boeing KC-135 Stratotanker is a military aerial refueling aircraft that was developed from the Boeing 367-80 prototype, alongside the Boeing 707 airliner.",
|
||||
"country-of-origin": "USA",
|
||||
"manufacturer": "Beoing",
|
||||
"role": "Tanker",
|
||||
"year-of-variant-introduction": "1957"
|
||||
}
|
||||
}],
|
||||
"KC135MPRS": [{
|
||||
"default": {
|
||||
"name": "KC-135 Stratotanker MPRS",
|
||||
"text": "The Boeing KC-135 Stratotanker is a military aerial refueling aircraft that was developed from the Boeing 367-80 prototype, alongside the Boeing 707 airliner. This model has the Multi-point Refueling System modification, allowing for probe and drogue refuelling.",
|
||||
"country-of-origin": "USA",
|
||||
"manufacturer": "Beoing",
|
||||
"role": "Tanker",
|
||||
"year-of-variant-introduction": "1994"
|
||||
}
|
||||
}],
|
||||
"L-39C": [{
|
||||
"default": {
|
||||
"name": "L-39C Albatros",
|
||||
@ -743,6 +773,16 @@
|
||||
"year-of-variant-introduction": "1984"
|
||||
}
|
||||
}],
|
||||
"S-3B Tanker": [{
|
||||
"default": {
|
||||
"name": "S-3B Tanker",
|
||||
"text": "The Lockheed S-3 Viking is a 4-crew, twin-engine turbofan-powered jet aircraft that was used by the U.S. Navy (USN) primarily for anti-submarine warfare. In the late 1990s, the S-3B's mission focus shifted to surface warfare and aerial refueling. The Viking also provided electronic warfare and surface surveillance capabilities to a carrier battle group. A carrier-based, subsonic, all-weather, long-range, multi-mission aircraft, it carried automated weapon systems and was capable of extended missions with in-flight refueling. Because of its characteristic sound, it was nicknamed the \"War Hoover\" after the vacuum cleaner brand. The S-3 was phased out from front-line fleet service aboard aircraft carriers in January 2009, with its missions taken over by aircraft like the P-3C Orion, P-8 Poseidon, Sikorsky SH-60 Seahawk and Boeing F/A-18E/F Super Hornet",
|
||||
"country-of-origin": "USA",
|
||||
"manufacturer": "Lockheed",
|
||||
"role": "Carrier-based Tanker",
|
||||
"year-of-variant-introduction": "1984"
|
||||
}
|
||||
}],
|
||||
"SA342L": [{
|
||||
"default": {
|
||||
"name": "SA 342L Gazelle",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user