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:
SnappyComebacks 2021-06-09 21:11:46 -07:00 committed by Dan Albert
parent a9dacf4a29
commit a53a648a63
31 changed files with 447 additions and 98 deletions

View File

@ -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

View File

@ -798,6 +798,7 @@ CARRIER_CAPABLE = [
Su_33,
A_4E_C,
S_3B,
S_3B_Tanker,
E_2C,
UH_1H,
Mi_8MT,

View File

@ -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", []))

View File

@ -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,
)

View File

@ -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

View File

@ -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")

View File

@ -81,6 +81,7 @@ class FrontLine(MissionTarget):
yield from [
FlightType.CAS,
FlightType.AEWC,
FlightType.REFUELING
# TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC
]

View File

@ -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:

View File

@ -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

View File

@ -183,6 +183,7 @@ class Package:
FlightType.TARCAP,
FlightType.BARCAP,
FlightType.AEWC,
FlightType.REFUELING,
FlightType.SWEEP,
FlightType.ESCORT,
]

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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):

View File

@ -32,7 +32,8 @@
],
"tankers": [
"KC_135",
"KC130"
"KC130",
"S-3B Tanker"
],
"frontline_units": [
"MBT_M1A2_Abrams",

View File

@ -33,7 +33,9 @@
],
"tankers": [
"KC_135",
"KC130"
"KC135MPRS",
"KC130",
"S-3B Tanker"
],
"frontline_units": [
"MBT_M1A2_Abrams",

View File

@ -28,7 +28,9 @@
],
"tankers": [
"KC_135",
"KC130"
"KC135MPRS",
"KC130",
"S-3B Tanker"
],
"frontline_units": [
"MBT_M1A2_Abrams",

View File

@ -15,6 +15,10 @@
"awacs": [
"E_2C"
],
"tankers": [
"KC_135",
"KC130"
],
"frontline_units": [
"MBT_M60A3_Patton",
"APC_M113",

View File

@ -18,6 +18,10 @@
"awacs": [
"E_2C"
],
"tankers": [
"KC_135",
"KC130"
],
"frontline_units": [
"MBT_M60A3_Patton",
"APC_M113",

View File

@ -32,7 +32,8 @@
],
"tankers": [
"KC_135",
"KC130"
"KC130",
"S-3B Tanker"
],
"frontline_units": [
"MBT_M1A2_Abrams",

View File

@ -32,7 +32,9 @@
],
"tankers": [
"KC_135",
"KC130"
"KC135MPRS",
"KC130",
"S-3B Tanker"
],
"frontline_units": [
"MBT_M1A2_Abrams",

View File

@ -33,7 +33,9 @@
],
"tankers": [
"KC_135",
"KC130"
"KC135MPRS",
"KC130",
"S-3B Tanker"
],
"frontline_units": [
"MBT_M1A2_Abrams",

View File

@ -28,7 +28,9 @@
],
"tankers": [
"KC_135",
"KC130"
"KC135MPRS",
"KC130",
"S-3B Tanker"
],
"frontline_units": [
"MBT_M1A2_Abrams",

View File

@ -17,7 +17,7 @@
"E_2C"
],
"tankers": [
"S_3B_Tanker"
"S-3B Tanker"
],
"frontline_units": [
"MBT_M60A3_Patton",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -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",