diff --git a/changelog.md b/changelog.md index f2d8f945..aa23a742 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/game/db.py b/game/db.py index 14063ca9..db9d8c26 100644 --- a/game/db.py +++ b/game/db.py @@ -798,6 +798,7 @@ CARRIER_CAPABLE = [ Su_33, A_4E_C, S_3B, + S_3B_Tanker, E_2C, UH_1H, Mi_8MT, diff --git a/game/factions/faction.py b/game/factions/faction.py index d17da280..cdb75ed0 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -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", [])) diff --git a/game/operation/operation.py b/game/operation/operation.py index 3df80df7..2f683f78 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -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, ) diff --git a/game/settings.py b/game/settings.py index e869283e..90723a55 100644 --- a/game/settings.py +++ b/game/settings.py @@ -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 diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 8cc6d56c..83d987e7 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -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") diff --git a/game/theater/frontline.py b/game/theater/frontline.py index 225980d1..8d46327c 100644 --- a/game/theater/frontline.py +++ b/game/theater/frontline.py @@ -81,6 +81,7 @@ class FrontLine(MissionTarget): yield from [ FlightType.CAS, FlightType.AEWC, + FlightType.REFUELING # TODO: FlightType.TROOP_TRANSPORT # TODO: FlightType.EVAC ] diff --git a/gen/aircraft.py b/gen/aircraft.py index bae04e7d..72d279fa 100644 --- a/gen/aircraft.py +++ b/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: diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 4943d0cf..c3f97791 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -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 diff --git a/gen/ato.py b/gen/ato.py index d3e991b2..52b59075 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -183,6 +183,7 @@ class Package: FlightType.TARCAP, FlightType.BARCAP, FlightType.AEWC, + FlightType.REFUELING, FlightType.SWEEP, FlightType.ESCORT, ] diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 58260dfd..2ba8cfec 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -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 diff --git a/gen/flights/ai_flight_planner_db.py b/gen/flights/ai_flight_planner_db.py index 7b982a2a..36486434 100644 --- a/gen/flights/ai_flight_planner_db.py +++ b/gen/flights/ai_flight_planner_db.py @@ -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: diff --git a/gen/flights/flight.py b/gen/flights/flight.py index fa826a30..bbdb6cd0 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -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 diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 90fa276c..f64e6ac5 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -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 diff --git a/qt_ui/windows/QUnitInfoWindow.py b/qt_ui/windows/QUnitInfoWindow.py index dca1aaf1..aaadd06d 100644 --- a/qt_ui/windows/QUnitInfoWindow.py +++ b/qt_ui/windows/QUnitInfoWindow.py @@ -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] diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 74afe47e..b5a6e7fe 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -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 diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index 88101b28..4c3ee0e0 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -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): diff --git a/resources/factions/NATO_Desert_Storm.json b/resources/factions/NATO_Desert_Storm.json index 0c02a3d1..785a86ef 100644 --- a/resources/factions/NATO_Desert_Storm.json +++ b/resources/factions/NATO_Desert_Storm.json @@ -32,7 +32,8 @@ ], "tankers": [ "KC_135", - "KC130" + "KC130", + "S-3B Tanker" ], "frontline_units": [ "MBT_M1A2_Abrams", diff --git a/resources/factions/bluefor_modern.json b/resources/factions/bluefor_modern.json index a07ba747..8f0ac073 100644 --- a/resources/factions/bluefor_modern.json +++ b/resources/factions/bluefor_modern.json @@ -33,7 +33,9 @@ ], "tankers": [ "KC_135", - "KC130" + "KC135MPRS", + "KC130", + "S-3B Tanker" ], "frontline_units": [ "MBT_M1A2_Abrams", diff --git a/resources/factions/us_aggressors.json b/resources/factions/us_aggressors.json index 38bab3fe..d8dc6f9e 100644 --- a/resources/factions/us_aggressors.json +++ b/resources/factions/us_aggressors.json @@ -28,7 +28,9 @@ ], "tankers": [ "KC_135", - "KC130" + "KC135MPRS", + "KC130", + "S-3B Tanker" ], "frontline_units": [ "MBT_M1A2_Abrams", diff --git a/resources/factions/usa_1965.json b/resources/factions/usa_1965.json index b0f08587..ebab742c 100644 --- a/resources/factions/usa_1965.json +++ b/resources/factions/usa_1965.json @@ -15,6 +15,10 @@ "awacs": [ "E_2C" ], + "tankers": [ + "KC_135", + "KC130" + ], "frontline_units": [ "MBT_M60A3_Patton", "APC_M113", diff --git a/resources/factions/usa_1975.json b/resources/factions/usa_1975.json index fc8f71cf..747245e1 100644 --- a/resources/factions/usa_1975.json +++ b/resources/factions/usa_1975.json @@ -18,6 +18,10 @@ "awacs": [ "E_2C" ], + "tankers": [ + "KC_135", + "KC130" + ], "frontline_units": [ "MBT_M60A3_Patton", "APC_M113", diff --git a/resources/factions/usa_1990.json b/resources/factions/usa_1990.json index 0a9d8913..dcb59b8f 100644 --- a/resources/factions/usa_1990.json +++ b/resources/factions/usa_1990.json @@ -32,7 +32,8 @@ ], "tankers": [ "KC_135", - "KC130" + "KC130", + "S-3B Tanker" ], "frontline_units": [ "MBT_M1A2_Abrams", diff --git a/resources/factions/usa_2005.json b/resources/factions/usa_2005.json index f516f47d..957555d3 100644 --- a/resources/factions/usa_2005.json +++ b/resources/factions/usa_2005.json @@ -32,7 +32,9 @@ ], "tankers": [ "KC_135", - "KC130" + "KC135MPRS", + "KC130", + "S-3B Tanker" ], "frontline_units": [ "MBT_M1A2_Abrams", diff --git a/resources/factions/usa_2005_c130.json b/resources/factions/usa_2005_c130.json index 8cfbcbb5..3523a6c9 100644 --- a/resources/factions/usa_2005_c130.json +++ b/resources/factions/usa_2005_c130.json @@ -33,7 +33,9 @@ ], "tankers": [ "KC_135", - "KC130" + "KC135MPRS", + "KC130", + "S-3B Tanker" ], "frontline_units": [ "MBT_M1A2_Abrams", diff --git a/resources/factions/usa_2005_modded.json b/resources/factions/usa_2005_modded.json index b791d905..821e832e 100644 --- a/resources/factions/usa_2005_modded.json +++ b/resources/factions/usa_2005_modded.json @@ -28,7 +28,9 @@ ], "tankers": [ "KC_135", - "KC130" + "KC135MPRS", + "KC130", + "S-3B Tanker" ], "frontline_units": [ "MBT_M1A2_Abrams", diff --git a/resources/factions/usn_1985.json b/resources/factions/usn_1985.json index ee7cb9f1..1b50eb12 100644 --- a/resources/factions/usn_1985.json +++ b/resources/factions/usn_1985.json @@ -17,7 +17,7 @@ "E_2C" ], "tankers": [ - "S_3B_Tanker" + "S-3B Tanker" ], "frontline_units": [ "MBT_M60A3_Patton", diff --git a/resources/ui/units/aircrafts/icons/KC130_24.jpg b/resources/ui/units/aircrafts/icons/KC130_24.jpg new file mode 100644 index 00000000..95301f5a Binary files /dev/null and b/resources/ui/units/aircrafts/icons/KC130_24.jpg differ diff --git a/resources/ui/units/aircrafts/icons/KC135MPRS_24.jpg b/resources/ui/units/aircrafts/icons/KC135MPRS_24.jpg new file mode 100644 index 00000000..18016ca2 Binary files /dev/null and b/resources/ui/units/aircrafts/icons/KC135MPRS_24.jpg differ diff --git a/resources/ui/units/aircrafts/icons/S-3B Tanker_24.jpg b/resources/ui/units/aircrafts/icons/S-3B Tanker_24.jpg new file mode 100644 index 00000000..b04d9ce6 Binary files /dev/null and b/resources/ui/units/aircrafts/icons/S-3B Tanker_24.jpg differ diff --git a/resources/units/unit_info_text.json b/resources/units/unit_info_text.json index 433ae1cd..812eb697 100644 --- a/resources/units/unit_info_text.json +++ b/resources/units/unit_info_text.json @@ -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",