From c65ac5a7cfb714bba7e0f34a92721dac6eb8d9f7 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 17 Jul 2021 15:31:52 -0700 Subject: [PATCH] Move mission range data into the aircraft type. The doctrine/task limits were capturing a reasonable average for the era, but it did a bad job for cases like the Harrier vs the Hornet, which perform similar missions but have drastically different max ranges. It also forced us into limiting CAS missions (even those flown by long range aircraft like the A-10) to 50nm since helicopters could commonly be fragged to them. This should allow us to design campaigns without needing airfields to be a max of ~50-100nm apart. --- changelog.md | 1 + game/commander/aircraftallocator.py | 18 +++++----- game/commander/missionproposals.py | 4 --- game/commander/packagebuilder.py | 2 +- game/commander/packagefulfiller.py | 1 - game/commander/tasks/packageplanningtask.py | 28 ++++------------ game/commander/tasks/primitive/aewc.py | 5 ++- game/commander/tasks/primitive/antiship.py | 12 ++----- .../commander/tasks/primitive/antishipping.py | 7 ++-- game/commander/tasks/primitive/bai.py | 7 ++-- game/commander/tasks/primitive/barcap.py | 5 ++- game/commander/tasks/primitive/cas.py | 7 ++-- .../tasks/primitive/convoyinterdiction.py | 6 ++-- game/commander/tasks/primitive/dead.py | 22 +++---------- game/commander/tasks/primitive/oca.py | 11 +++---- game/commander/tasks/primitive/refueling.py | 5 ++- game/commander/tasks/primitive/strike.py | 7 ++-- game/data/doctrine.py | 19 ++++------- game/dcs/aircrafttype.py | 19 +++++++++-- game/procurement.py | 33 +++++++------------ game/transfers.py | 4 +-- resources/units/aircraft/A-50.yaml | 1 + resources/units/aircraft/AV8BNA.yaml | 1 + resources/units/aircraft/An-26B.yaml | 1 + resources/units/aircraft/B-1B.yaml | 4 ++- resources/units/aircraft/B-52H.yaml | 4 ++- resources/units/aircraft/C-130.yaml | 1 + resources/units/aircraft/C-17A.yaml | 1 + resources/units/aircraft/E-2C.yaml | 1 + resources/units/aircraft/E-3A.yaml | 1 + resources/units/aircraft/F-14A-135-GR.yaml | 1 + resources/units/aircraft/F-14B.yaml | 1 + resources/units/aircraft/F-16A.yaml | 1 + resources/units/aircraft/F-16C_50.yaml | 1 + resources/units/aircraft/Hercules.yaml | 4 ++- resources/units/aircraft/IL-76MD.yaml | 1 + resources/units/aircraft/IL-78M.yaml | 1 + resources/units/aircraft/KC-135.yaml | 1 + resources/units/aircraft/KC130.yaml | 4 ++- resources/units/aircraft/KC135MPRS.yaml | 4 ++- resources/units/aircraft/KJ-2000.yaml | 1 + resources/units/aircraft/S-3B Tanker.yaml | 4 ++- resources/units/aircraft/Yak-40.yaml | 1 + 43 files changed, 120 insertions(+), 143 deletions(-) diff --git a/changelog.md b/changelog.md index 4b131551..e0e61174 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ Saves from 3.x are not compatible with 5.0. * **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions. * **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI. * **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points. +* **[Campaign AI]** Auto-planning mission range limits are now specified per-aircraft. On average this means that longer range missions will now be plannable. The limit only accounts for the direct distance to the target, not the path taken. * **[New Game Wizard]** Can now customize the player's air wing before campaign start to disable or rename squadrons. ## Fixes diff --git a/game/commander/aircraftallocator.py b/game/commander/aircraftallocator.py index 16ea678a..523fad64 100644 --- a/game/commander/aircraftallocator.py +++ b/game/commander/aircraftallocator.py @@ -3,7 +3,8 @@ from typing import Optional, Tuple from game.commander.missionproposals import ProposedFlight from game.inventory import GlobalAircraftInventory from game.squadrons import AirWing, Squadron -from game.theater import ControlPoint +from game.theater import ControlPoint, MissionTarget +from game.utils import meters from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.closestairfields import ClosestAirfields from gen.flights.flight import FlightType @@ -25,7 +26,7 @@ class AircraftAllocator: self.is_player = is_player def find_squadron_for_flight( - self, flight: ProposedFlight + self, target: MissionTarget, flight: ProposedFlight ) -> Optional[Tuple[ControlPoint, Squadron]]: """Finds aircraft suitable for the given mission. @@ -45,17 +46,13 @@ class AircraftAllocator: on subsequent calls. If the found aircraft are not used, the caller is responsible for returning them to the inventory. """ - return self.find_aircraft_for_task(flight, flight.task) + return self.find_aircraft_for_task(target, flight, flight.task) def find_aircraft_for_task( - self, flight: ProposedFlight, task: FlightType + self, target: MissionTarget, flight: ProposedFlight, task: FlightType ) -> Optional[Tuple[ControlPoint, Squadron]]: types = aircraft_for_task(task) - airfields_in_range = self.closest_airfields.operational_airfields_within( - flight.max_distance - ) - - for airfield in airfields_in_range: + for airfield in self.closest_airfields.operational_airfields: if not airfield.is_friendly(self.is_player): continue inventory = self.global_inventory.for_control_point(airfield) @@ -64,6 +61,9 @@ class AircraftAllocator: continue if inventory.available(aircraft) < flight.num_aircraft: continue + distance_to_target = meters(target.distance_to(airfield)) + if distance_to_target > aircraft.max_mission_range: + continue # Valid location with enough aircraft available. Find a squadron to fit # the role. squadrons = self.air_wing.auto_assignable_for_task_with_type( diff --git a/game/commander/missionproposals.py b/game/commander/missionproposals.py index 2b8fc074..a13802b8 100644 --- a/game/commander/missionproposals.py +++ b/game/commander/missionproposals.py @@ -3,7 +3,6 @@ from enum import Enum, auto from typing import Optional from game.theater import MissionTarget -from game.utils import Distance from gen.flights.flight import FlightType @@ -27,9 +26,6 @@ class ProposedFlight: #: The number of aircraft required. num_aircraft: int - #: The maximum distance between the objective and the departure airfield. - max_distance: Distance - #: The type of threat this flight defends against if it is an escort. Escort #: flights will be pruned if the rest of the package is not threatened by #: the threat they defend against. If this flight is not an escort, this diff --git a/game/commander/packagebuilder.py b/game/commander/packagebuilder.py index 490e0286..da96a8e2 100644 --- a/game/commander/packagebuilder.py +++ b/game/commander/packagebuilder.py @@ -44,7 +44,7 @@ class PackageBuilder: caller should return any previously planned flights to the inventory using release_planned_aircraft. """ - assignment = self.allocator.find_squadron_for_flight(plan) + assignment = self.allocator.find_squadron_for_flight(self.package.target, plan) if assignment is None: return False airfield, squadron = assignment diff --git a/game/commander/packagefulfiller.py b/game/commander/packagefulfiller.py index d4d8352b..1005bfa9 100644 --- a/game/commander/packagefulfiller.py +++ b/game/commander/packagefulfiller.py @@ -83,7 +83,6 @@ class PackageFulfiller: missing_types.add(flight.task) purchase_order = AircraftProcurementRequest( near=mission.location, - range=flight.max_distance, task_capability=flight.task, number=flight.num_aircraft, ) diff --git a/game/commander/tasks/packageplanningtask.py b/game/commander/tasks/packageplanningtask.py index fb50af23..8e2eb8a2 100644 --- a/game/commander/tasks/packageplanningtask.py +++ b/game/commander/tasks/packageplanningtask.py @@ -59,28 +59,23 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]): coalition.ato.add_package(self.package) @abstractmethod - def propose_flights(self, doctrine: Doctrine) -> None: + def propose_flights(self) -> None: ... def propose_flight( self, task: FlightType, num_aircraft: int, - max_distance: Optional[Distance], escort_type: Optional[EscortType] = None, ) -> None: - if max_distance is None: - max_distance = Distance.inf() - self.flights.append( - ProposedFlight(task, num_aircraft, max_distance, escort_type) - ) + self.flights.append(ProposedFlight(task, num_aircraft, escort_type)) @property def asap(self) -> bool: return False def fulfill_mission(self, state: TheaterState) -> bool: - self.propose_flights(state.context.coalition.doctrine) + self.propose_flights() fulfiller = PackageFulfiller( state.context.coalition, state.context.theater, @@ -92,20 +87,9 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]): ) return self.package is not None - def propose_common_escorts(self, doctrine: Doctrine) -> None: - self.propose_flight( - FlightType.SEAD_ESCORT, - 2, - doctrine.mission_ranges.offensive, - EscortType.Sead, - ) - - self.propose_flight( - FlightType.ESCORT, - 2, - doctrine.mission_ranges.offensive, - EscortType.AirToAir, - ) + def propose_common_escorts(self) -> None: + self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead) + self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir) def iter_iads_ranges( self, state: TheaterState, range_type: RangeType diff --git a/game/commander/tasks/primitive/aewc.py b/game/commander/tasks/primitive/aewc.py index 8153aac6..f9c6a7d2 100644 --- a/game/commander/tasks/primitive/aewc.py +++ b/game/commander/tasks/primitive/aewc.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.theater import MissionTarget from gen.flights.flight import FlightType @@ -19,8 +18,8 @@ class PlanAewc(PackagePlanningTask[MissionTarget]): def apply_effects(self, state: TheaterState) -> None: state.aewc_targets.remove(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.AEWC, 1, doctrine.mission_ranges.aewc) + def propose_flights(self) -> None: + self.propose_flight(FlightType.AEWC, 1) @property def asap(self) -> bool: diff --git a/game/commander/tasks/primitive/antiship.py b/game/commander/tasks/primitive/antiship.py index 3f85c74c..a135e1cd 100644 --- a/game/commander/tasks/primitive/antiship.py +++ b/game/commander/tasks/primitive/antiship.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from game.commander.missionproposals import EscortType from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.theater.theatergroundobject import NavalGroundObject from gen.flights.flight import FlightType @@ -22,11 +21,6 @@ class PlanAntiShip(PackagePlanningTask[NavalGroundObject]): def apply_effects(self, state: TheaterState) -> None: state.eliminate_ship(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.ANTISHIP, 2, doctrine.mission_ranges.offensive) - self.propose_flight( - FlightType.ESCORT, - 2, - doctrine.mission_ranges.offensive, - EscortType.AirToAir, - ) + def propose_flights(self) -> None: + self.propose_flight(FlightType.ANTISHIP, 2) + self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir) diff --git a/game/commander/tasks/primitive/antishipping.py b/game/commander/tasks/primitive/antishipping.py index 303a9af1..64279d1b 100644 --- a/game/commander/tasks/primitive/antishipping.py +++ b/game/commander/tasks/primitive/antishipping.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.transfers import CargoShip from gen.flights.flight import FlightType @@ -21,6 +20,6 @@ class PlanAntiShipping(PackagePlanningTask[CargoShip]): def apply_effects(self, state: TheaterState) -> None: state.enemy_shipping.remove(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.ANTISHIP, 2, doctrine.mission_ranges.offensive) - self.propose_common_escorts(doctrine) + def propose_flights(self) -> None: + self.propose_flight(FlightType.ANTISHIP, 2) + self.propose_common_escorts() diff --git a/game/commander/tasks/primitive/bai.py b/game/commander/tasks/primitive/bai.py index f9d61818..4878171d 100644 --- a/game/commander/tasks/primitive/bai.py +++ b/game/commander/tasks/primitive/bai.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.theater.theatergroundobject import VehicleGroupGroundObject from gen.flights.flight import FlightType @@ -21,6 +20,6 @@ class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]): def apply_effects(self, state: TheaterState) -> None: state.eliminate_garrison(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive) - self.propose_common_escorts(doctrine) + def propose_flights(self) -> None: + self.propose_flight(FlightType.BAI, 2) + self.propose_common_escorts() diff --git a/game/commander/tasks/primitive/barcap.py b/game/commander/tasks/primitive/barcap.py index 77302adf..c2dafae7 100644 --- a/game/commander/tasks/primitive/barcap.py +++ b/game/commander/tasks/primitive/barcap.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.theater import ControlPoint from gen.flights.flight import FlightType @@ -19,5 +18,5 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]): def apply_effects(self, state: TheaterState) -> None: state.barcaps_needed[self.target] -= 1 - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.BARCAP, 2, doctrine.mission_ranges.cap) + def propose_flights(self) -> None: + self.propose_flight(FlightType.BARCAP, 2) diff --git a/game/commander/tasks/primitive/cas.py b/game/commander/tasks/primitive/cas.py index 7a9997ff..c2785405 100644 --- a/game/commander/tasks/primitive/cas.py +++ b/game/commander/tasks/primitive/cas.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.theater import FrontLine from gen.flights.flight import FlightType @@ -19,6 +18,6 @@ class PlanCas(PackagePlanningTask[FrontLine]): def apply_effects(self, state: TheaterState) -> None: state.vulnerable_front_lines.remove(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.CAS, 2, doctrine.mission_ranges.cas) - self.propose_flight(FlightType.TARCAP, 2, doctrine.mission_ranges.cap) + def propose_flights(self) -> None: + self.propose_flight(FlightType.CAS, 2) + self.propose_flight(FlightType.TARCAP, 2) diff --git a/game/commander/tasks/primitive/convoyinterdiction.py b/game/commander/tasks/primitive/convoyinterdiction.py index 11ed4ee4..285326c7 100644 --- a/game/commander/tasks/primitive/convoyinterdiction.py +++ b/game/commander/tasks/primitive/convoyinterdiction.py @@ -21,6 +21,6 @@ class PlanConvoyInterdiction(PackagePlanningTask[Convoy]): def apply_effects(self, state: TheaterState) -> None: state.enemy_convoys.remove(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive) - self.propose_common_escorts(doctrine) + def propose_flights(self) -> None: + self.propose_flight(FlightType.BAI, 2) + self.propose_common_escorts() diff --git a/game/commander/tasks/primitive/dead.py b/game/commander/tasks/primitive/dead.py index 3861908c..45da3cc3 100644 --- a/game/commander/tasks/primitive/dead.py +++ b/game/commander/tasks/primitive/dead.py @@ -5,7 +5,6 @@ from dataclasses import dataclass from game.commander.missionproposals import EscortType from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.theater.theatergroundobject import IadsGroundObject from gen.flights.flight import FlightType @@ -25,8 +24,8 @@ class PlanDead(PackagePlanningTask[IadsGroundObject]): def apply_effects(self, state: TheaterState) -> None: state.eliminate_air_defense(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.DEAD, 2, doctrine.mission_ranges.offensive) + def propose_flights(self) -> None: + self.propose_flight(FlightType.DEAD, 2) # Only include SEAD against SAMs that still have emitters. No need to # suppress an EWR, and SEAD isn't useful against a SAM that no longer has a @@ -41,18 +40,7 @@ class PlanDead(PackagePlanningTask[IadsGroundObject]): # package is *only* threatened by the target though. Could be improved, but # needs a decent refactor to the escort planning to do so. if self.target.has_live_radar_sam: - self.propose_flight(FlightType.SEAD, 2, doctrine.mission_ranges.offensive) + self.propose_flight(FlightType.SEAD, 2) else: - self.propose_flight( - FlightType.SEAD_ESCORT, - 2, - doctrine.mission_ranges.offensive, - EscortType.Sead, - ) - - self.propose_flight( - FlightType.ESCORT, - 2, - doctrine.mission_ranges.offensive, - EscortType.AirToAir, - ) + self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead) + self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir) diff --git a/game/commander/tasks/primitive/oca.py b/game/commander/tasks/primitive/oca.py index 4c995f75..be88df32 100644 --- a/game/commander/tasks/primitive/oca.py +++ b/game/commander/tasks/primitive/oca.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.theater import ControlPoint from gen.flights.flight import FlightType @@ -23,10 +22,8 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]): def apply_effects(self, state: TheaterState) -> None: state.oca_targets.remove(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.OCA_RUNWAY, 2, doctrine.mission_ranges.offensive) + def propose_flights(self) -> None: + self.propose_flight(FlightType.OCA_RUNWAY, 2) if self.aircraft_cold_start: - self.propose_flight( - FlightType.OCA_AIRCRAFT, 2, doctrine.mission_ranges.offensive - ) - self.propose_common_escorts(doctrine) + self.propose_flight(FlightType.OCA_AIRCRAFT, 2) + self.propose_common_escorts() diff --git a/game/commander/tasks/primitive/refueling.py b/game/commander/tasks/primitive/refueling.py index 005cbc3a..5f17f3df 100644 --- a/game/commander/tasks/primitive/refueling.py +++ b/game/commander/tasks/primitive/refueling.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.theater import MissionTarget from gen.flights.flight import FlightType @@ -19,5 +18,5 @@ class PlanRefueling(PackagePlanningTask[MissionTarget]): def apply_effects(self, state: TheaterState) -> None: state.refueling_targets.remove(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.REFUELING, 1, doctrine.mission_ranges.refueling) + def propose_flights(self) -> None: + self.propose_flight(FlightType.REFUELING, 1) diff --git a/game/commander/tasks/primitive/strike.py b/game/commander/tasks/primitive/strike.py index ce322dad..e89c9cac 100644 --- a/game/commander/tasks/primitive/strike.py +++ b/game/commander/tasks/primitive/strike.py @@ -5,7 +5,6 @@ from typing import Any from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.theaterstate import TheaterState -from game.data.doctrine import Doctrine from game.theater.theatergroundobject import TheaterGroundObject from gen.flights.flight import FlightType @@ -22,6 +21,6 @@ class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]): def apply_effects(self, state: TheaterState) -> None: state.strike_targets.remove(self.target) - def propose_flights(self, doctrine: Doctrine) -> None: - self.propose_flight(FlightType.STRIKE, 2, doctrine.mission_ranges.offensive) - self.propose_common_escorts(doctrine) + def propose_flights(self) -> None: + self.propose_flight(FlightType.STRIKE, 2) + self.propose_common_escorts() diff --git a/game/data/doctrine.py b/game/data/doctrine.py index 21402501..359a1435 100644 --- a/game/data/doctrine.py +++ b/game/data/doctrine.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -18,15 +18,6 @@ class GroundUnitProcurementRatios: return 0.0 -@dataclass(frozen=True) -class MissionPlannerMaxRanges: - cap: Distance = field(default=nautical_miles(100)) - cas: Distance = field(default=nautical_miles(50)) - offensive: Distance = field(default=nautical_miles(150)) - aewc: Distance = field(default=Distance.inf()) - refueling: Distance = field(default=nautical_miles(200)) - - @dataclass(frozen=True) class Doctrine: cas: bool @@ -88,8 +79,6 @@ class Doctrine: ground_unit_procurement_ratios: GroundUnitProcurementRatios - mission_ranges: MissionPlannerMaxRanges = field(default=MissionPlannerMaxRanges()) - @has_save_compat_for(5) def __setstate__(self, state: dict[str, Any]) -> None: if "max_ingress_distance" not in state: @@ -111,6 +100,12 @@ class Doctrine: self.__dict__.update(state) +class MissionPlannerMaxRanges: + @has_save_compat_for(5) + def __init__(self) -> None: + pass + + MODERN_DOCTRINE = Doctrine( cap=True, cas=True, diff --git a/game/dcs/aircrafttype.py b/game/dcs/aircrafttype.py index dd9b5282..56fa3f0f 100644 --- a/game/dcs/aircrafttype.py +++ b/game/dcs/aircrafttype.py @@ -29,7 +29,7 @@ from game.radio.channels import ( ViggenRadioChannelAllocator, NoOpChannelAllocator, ) -from game.utils import Distance, Speed, feet, kph, knots +from game.utils import Distance, Speed, feet, kph, knots, nautical_miles if TYPE_CHECKING: from gen.aircraft import FlightData @@ -112,13 +112,18 @@ class AircraftType(UnitType[Type[FlyingType]]): lha_capable: bool always_keeps_gun: bool - # If true, the aircraft does not use the guns as the last resort weapons, but as a main weapon. - # It'll RTB when it doesn't have gun ammo left. + # If true, the aircraft does not use the guns as the last resort weapons, but as a + # main weapon. It'll RTB when it doesn't have gun ammo left. gunfighter: bool max_group_size: int patrol_altitude: Optional[Distance] patrol_speed: Optional[Speed] + + #: The maximum range between the origin airfield and the target for which the auto- + #: planner will consider this aircraft usable for a mission. + max_mission_range: Distance + intra_flight_radio: Optional[Radio] channel_allocator: Optional[RadioChannelAllocator] channel_namer: Type[ChannelNamer] @@ -230,6 +235,13 @@ class AircraftType(UnitType[Type[FlyingType]]): radio_config = RadioConfig.from_data(data.get("radios", {})) patrol_config = PatrolConfig.from_data(data.get("patrol", {})) + try: + mission_range = nautical_miles(int(data["max_range"])) + except (KeyError, ValueError): + mission_range = ( + nautical_miles(50) if aircraft.helicopter else nautical_miles(150) + ) + try: introduction = data["introduced"] if introduction is None: @@ -257,6 +269,7 @@ class AircraftType(UnitType[Type[FlyingType]]): max_group_size=data.get("max_group_size", aircraft.group_size_max), patrol_altitude=patrol_config.altitude, patrol_speed=patrol_config.speed, + max_mission_range=mission_range, intra_flight_radio=radio_config.intra_flight, channel_allocator=radio_config.channel_allocator, channel_namer=radio_config.channel_namer, diff --git a/game/procurement.py b/game/procurement.py index 8820453c..3b1ea370 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -11,7 +11,7 @@ from game.dcs.aircrafttype import AircraftType from game.dcs.groundunittype import GroundUnitType from game.factions.faction import Faction from game.theater import ControlPoint, MissionTarget -from game.utils import Distance +from game.utils import meters from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.flight import FlightType @@ -25,15 +25,13 @@ FRONTLINE_RESERVES_FACTOR = 1.3 @dataclass(frozen=True) class AircraftProcurementRequest: near: MissionTarget - range: Distance task_capability: FlightType number: int def __str__(self) -> str: task = self.task_capability.value - distance = self.range.nautical_miles target = self.near.name - return f"{self.number} ship {task} within {distance} nm of {target}" + return f"{self.number} ship {task} near {target}" class ProcurementAi: @@ -211,24 +209,24 @@ class ProcurementAi: return GroundUnitClass.Tank return worst_balanced - def _affordable_aircraft_for_task( - self, - task: FlightType, - airbase: ControlPoint, - number: int, - max_price: float, + def affordable_aircraft_for( + self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float ) -> Optional[AircraftType]: best_choice: Optional[AircraftType] = None - for unit in aircraft_for_task(task): + for unit in aircraft_for_task(request.task_capability): if unit not in self.faction.aircrafts: continue - if unit.price * number > max_price: + if unit.price * request.number > budget: continue if not airbase.can_operate(unit): continue + distance_to_target = meters(request.near.distance_to(airbase)) + if distance_to_target > unit.max_mission_range: + continue + for squadron in self.air_wing.squadrons_for(unit): - if task in squadron.auto_assignable_mission_types: + if request.task_capability in squadron.auto_assignable_mission_types: break else: continue @@ -241,13 +239,6 @@ class ProcurementAi: break return best_choice - def affordable_aircraft_for( - self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float - ) -> Optional[AircraftType]: - return self._affordable_aircraft_for_task( - request.task_capability, airbase, request.number, budget - ) - def fulfill_aircraft_request( self, request: AircraftProcurementRequest, budget: float ) -> Tuple[float, bool]: @@ -293,7 +284,7 @@ class ProcurementAi: ) -> Iterator[ControlPoint]: distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near) threatened = [] - for cp in distance_cache.operational_airfields_within(request.range): + for cp in distance_cache.operational_airfields: if not cp.is_friendly(self.is_player): continue if cp.unclaimed_parking(self.game) < request.number: diff --git a/game/transfers.py b/game/transfers.py index 7401b03d..68e8dea1 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -688,7 +688,5 @@ class PendingTransfers: gap += 1 self.game.procurement_requests_for(self.player).append( - AircraftProcurementRequest( - control_point, nautical_miles(200), FlightType.TRANSPORT, gap - ) + AircraftProcurementRequest(control_point, FlightType.TRANSPORT, gap) ) diff --git a/resources/units/aircraft/A-50.yaml b/resources/units/aircraft/A-50.yaml index 574f8cd9..bef84e03 100644 --- a/resources/units/aircraft/A-50.yaml +++ b/resources/units/aircraft/A-50.yaml @@ -1,5 +1,6 @@ description: The A-50 is an AWACS plane. max_group_size: 1 +max_range: 2000 price: 50 patrol: altitude: 33000 diff --git a/resources/units/aircraft/AV8BNA.yaml b/resources/units/aircraft/AV8BNA.yaml index 7e4fec82..3accfba7 100644 --- a/resources/units/aircraft/AV8BNA.yaml +++ b/resources/units/aircraft/AV8BNA.yaml @@ -27,6 +27,7 @@ manufacturer: McDonnell Douglas origin: USA/UK price: 15 role: V/STOL Attack +max_range: 100 variants: AV-8B Harrier II Night Attack: {} radios: diff --git a/resources/units/aircraft/An-26B.yaml b/resources/units/aircraft/An-26B.yaml index 4ed84aa7..01f90c19 100644 --- a/resources/units/aircraft/An-26B.yaml +++ b/resources/units/aircraft/An-26B.yaml @@ -1,4 +1,5 @@ description: The An-26B is a military transport aircraft. price: 15 +max_range: 800 variants: An-26B: null diff --git a/resources/units/aircraft/B-1B.yaml b/resources/units/aircraft/B-1B.yaml index 0d34fb01..3c309c7b 100644 --- a/resources/units/aircraft/B-1B.yaml +++ b/resources/units/aircraft/B-1B.yaml @@ -1,4 +1,5 @@ -description: The Rockwell B-1 Lancer is a supersonic variable-sweep wing, heavy bomber +description: + The Rockwell B-1 Lancer is a supersonic variable-sweep wing, heavy bomber used by the United States Air Force. It is commonly called the 'Bone' (from 'B-One').It is one of three strategic bombers in the U.S. Air Force fleet as of 2021, the other two being the B-2 Spirit and the B-52 Stratofortress. It first served in combat @@ -12,5 +13,6 @@ manufacturer: Rockwell origin: USA price: 45 role: Supersonic Strategic Bomber +max_range: 2000 variants: B-1B Lancer: {} diff --git a/resources/units/aircraft/B-52H.yaml b/resources/units/aircraft/B-52H.yaml index 65aa01c0..7221fd5e 100644 --- a/resources/units/aircraft/B-52H.yaml +++ b/resources/units/aircraft/B-52H.yaml @@ -1,4 +1,5 @@ -description: The Boeing B-52 Stratofortress is capable of carrying up to 70,000 pounds +description: + The Boeing B-52 Stratofortress is capable of carrying up to 70,000 pounds (32,000 kg) of weapons, and has a typical combat range of more than 8,800 miles (14,080 km) without aerial refueling. The B-52 completed sixty years of continuous service with its original operator in 2015. After being upgraded between 2013 and @@ -8,5 +9,6 @@ manufacturer: Boeing origin: USA price: 35 role: Strategic Bomber +max_range: 2000 variants: B-52H Stratofortress: {} diff --git a/resources/units/aircraft/C-130.yaml b/resources/units/aircraft/C-130.yaml index 4efe0d0a..eca68ffa 100644 --- a/resources/units/aircraft/C-130.yaml +++ b/resources/units/aircraft/C-130.yaml @@ -1,4 +1,5 @@ description: The C-130 is a military transport aircraft. price: 15 +max_range: 1000 variants: C-130: null diff --git a/resources/units/aircraft/C-17A.yaml b/resources/units/aircraft/C-17A.yaml index a121e07b..692e24a9 100644 --- a/resources/units/aircraft/C-17A.yaml +++ b/resources/units/aircraft/C-17A.yaml @@ -1,4 +1,5 @@ description: The C-17 is a military transport aircraft. price: 18 +max_range: 2000 variants: C-17A: null diff --git a/resources/units/aircraft/E-2C.yaml b/resources/units/aircraft/E-2C.yaml index ca25d97e..7154813a 100644 --- a/resources/units/aircraft/E-2C.yaml +++ b/resources/units/aircraft/E-2C.yaml @@ -8,6 +8,7 @@ manufacturer: Northrop Grumman origin: USA price: 50 role: AEW&C +max_range: 2000 patrol: altitude: 30000 variants: diff --git a/resources/units/aircraft/E-3A.yaml b/resources/units/aircraft/E-3A.yaml index ca781a23..a8e676e4 100644 --- a/resources/units/aircraft/E-3A.yaml +++ b/resources/units/aircraft/E-3A.yaml @@ -1,6 +1,7 @@ description: The E-3A is a AWACS aicraft. price: 50 max_group_size: 1 +max_range: 2000 patrol: altitude: 35000 variants: diff --git a/resources/units/aircraft/F-14A-135-GR.yaml b/resources/units/aircraft/F-14A-135-GR.yaml index eb593105..b467d7e9 100644 --- a/resources/units/aircraft/F-14A-135-GR.yaml +++ b/resources/units/aircraft/F-14A-135-GR.yaml @@ -21,6 +21,7 @@ manufacturer: Grumman origin: USA price: 22 role: Carrier-based Air-Superiority Fighter/Fighter Bomber +max_range: 250 variants: F-14A Tomcat (Block 135-GR Late): {} radios: diff --git a/resources/units/aircraft/F-14B.yaml b/resources/units/aircraft/F-14B.yaml index a6244a4a..2cc64fa4 100644 --- a/resources/units/aircraft/F-14B.yaml +++ b/resources/units/aircraft/F-14B.yaml @@ -21,6 +21,7 @@ manufacturer: Grumman origin: USA price: 26 role: Carrier-based Air-Superiority Fighter/Fighter Bomber +max_range: 250 variants: F-14B Tomcat: {} radios: diff --git a/resources/units/aircraft/F-16A.yaml b/resources/units/aircraft/F-16A.yaml index 99c2c2e5..fdfcdb5c 100644 --- a/resources/units/aircraft/F-16A.yaml +++ b/resources/units/aircraft/F-16A.yaml @@ -1,4 +1,5 @@ description: The early verison of the F-16. It flew in Desert Storm. price: 15 +max_range: 200 variants: F-16A: null diff --git a/resources/units/aircraft/F-16C_50.yaml b/resources/units/aircraft/F-16C_50.yaml index 9e5b3740..adefe831 100644 --- a/resources/units/aircraft/F-16C_50.yaml +++ b/resources/units/aircraft/F-16C_50.yaml @@ -27,6 +27,7 @@ manufacturer: General Dynamics origin: USA price: 22 role: Multirole Fighter +max_range: 200 variants: F-16CM Fighting Falcon (Block 50): {} F-2A: {} diff --git a/resources/units/aircraft/Hercules.yaml b/resources/units/aircraft/Hercules.yaml index 070409fa..af82aaa6 100644 --- a/resources/units/aircraft/Hercules.yaml +++ b/resources/units/aircraft/Hercules.yaml @@ -1,4 +1,5 @@ -description: The Lockheed Martin C-130J Super Hercules is a four-engine turboprop +description: + The Lockheed Martin C-130J Super Hercules is a four-engine turboprop military transport aircraft. The C-130J is a comprehensive update of the Lockheed C-130 Hercules, with new engines, flight deck, and other systems. As of February 2018, 400 C-130J aircraft have been delivered to 17 nations. @@ -7,5 +8,6 @@ manufacturer: Lockheed origin: USA price: 18 role: Transport +max_range: 1000 variants: C-130J-30 Super Hercules: {} diff --git a/resources/units/aircraft/IL-76MD.yaml b/resources/units/aircraft/IL-76MD.yaml index 97020aca..74ca1ab1 100644 --- a/resources/units/aircraft/IL-76MD.yaml +++ b/resources/units/aircraft/IL-76MD.yaml @@ -1,3 +1,4 @@ price: 20 +max_range: 1000 variants: IL-76MD: null diff --git a/resources/units/aircraft/IL-78M.yaml b/resources/units/aircraft/IL-78M.yaml index 2acd1ea3..de5b76f2 100644 --- a/resources/units/aircraft/IL-78M.yaml +++ b/resources/units/aircraft/IL-78M.yaml @@ -1,5 +1,6 @@ price: 20 max_group_size: 1 +max_range: 1000 patrol: # ~280 knots IAS. speed: 400 diff --git a/resources/units/aircraft/KC-135.yaml b/resources/units/aircraft/KC-135.yaml index 138ae873..2cb5e40d 100644 --- a/resources/units/aircraft/KC-135.yaml +++ b/resources/units/aircraft/KC-135.yaml @@ -8,6 +8,7 @@ manufacturer: Beoing origin: USA price: 25 role: Tanker +max_range: 1000 patrol: # ~300 knots IAS. speed: 445 diff --git a/resources/units/aircraft/KC130.yaml b/resources/units/aircraft/KC130.yaml index 802a16bb..5b9ffdca 100644 --- a/resources/units/aircraft/KC130.yaml +++ b/resources/units/aircraft/KC130.yaml @@ -1,10 +1,12 @@ -description: The Lockheed Martin (previously Lockheed) KC-130 is a family of the extended-range +description: + 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. introduced: 1962 manufacturer: Lockheed Martin origin: USA price: 25 role: Tanker +max_range: 1000 patrol: # ~210 knots IAS, roughly the max for the KC-130 at altitude. speed: 370 diff --git a/resources/units/aircraft/KC135MPRS.yaml b/resources/units/aircraft/KC135MPRS.yaml index 4ba28ff5..c59d3098 100644 --- a/resources/units/aircraft/KC135MPRS.yaml +++ b/resources/units/aircraft/KC135MPRS.yaml @@ -1,4 +1,5 @@ -description: The Boeing KC-135 Stratotanker is a military aerial refueling aircraft +description: + 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. @@ -7,6 +8,7 @@ manufacturer: Boeing origin: USA price: 25 role: Tanker +max_range: 1000 patrol: # 300 knots IAS. speed: 440 diff --git a/resources/units/aircraft/KJ-2000.yaml b/resources/units/aircraft/KJ-2000.yaml index cbb843c8..8078c359 100644 --- a/resources/units/aircraft/KJ-2000.yaml +++ b/resources/units/aircraft/KJ-2000.yaml @@ -1,4 +1,5 @@ price: 50 +max_range: 2000 patrol: altitude: 40000 variants: diff --git a/resources/units/aircraft/S-3B Tanker.yaml b/resources/units/aircraft/S-3B Tanker.yaml index dabe7056..6a4680e3 100644 --- a/resources/units/aircraft/S-3B Tanker.yaml +++ b/resources/units/aircraft/S-3B Tanker.yaml @@ -1,5 +1,6 @@ carrier_capable: true -description: The Lockheed S-3 Viking is a 4-crew, twin-engine turbofan-powered jet +description: + 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 @@ -16,6 +17,7 @@ origin: USA price: 20 max_group_size: 1 role: Carrier-based Tanker +max_range: 1000 patrol: # ~265 knots IAS. speed: 320 diff --git a/resources/units/aircraft/Yak-40.yaml b/resources/units/aircraft/Yak-40.yaml index d56a2b65..d69242fc 100644 --- a/resources/units/aircraft/Yak-40.yaml +++ b/resources/units/aircraft/Yak-40.yaml @@ -1,3 +1,4 @@ price: 25 +max_range: 600 variants: Yak-40: null