diff --git a/changelog.md b/changelog.md index 0d4174d9..203eaeac 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,7 @@ Saves from 3.x are not compatible with 5.0. * **[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. +* **[Campaign AI]** Aircraft will now only be automatically purchased or assigned at appropriate bases. Naval aircraft will default to only operating from carriers, Harriers will default to LHAs and shore bases, helicopters will operate from anywhere. This can be customized per-squadron. * **[Kneeboard]** Minimum required fuel estimates have been added to the kneeboard for aircraft with supporting data (currently only the Hornet). * **[New Game Wizard]** Can now customize the player's air wing before campaign start to disable or rename squadrons. diff --git a/game/commander/aircraftallocator.py b/game/commander/aircraftallocator.py index 523fad64..a50dbd22 100644 --- a/game/commander/aircraftallocator.py +++ b/game/commander/aircraftallocator.py @@ -70,7 +70,9 @@ class AircraftAllocator: aircraft, task ) for squadron in squadrons: - if squadron.can_provide_pilots(flight.num_aircraft): + if squadron.operates_from(airfield) and squadron.can_provide_pilots( + flight.num_aircraft + ): inventory.remove_aircraft(aircraft, flight.num_aircraft) return airfield, squadron return None diff --git a/game/procurement.py b/game/procurement.py index 3b1ea370..d1c254f0 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -226,7 +226,11 @@ class ProcurementAi: continue for squadron in self.air_wing.squadrons_for(unit): - if request.task_capability in squadron.auto_assignable_mission_types: + if ( + squadron.operates_from(airbase) + and request.task_capability + in squadron.auto_assignable_mission_types + ): break else: continue diff --git a/game/squadrons.py b/game/squadrons.py index 45ebe7de..5777102f 100644 --- a/game/squadrons.py +++ b/game/squadrons.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import itertools import logging import random @@ -26,6 +27,7 @@ if TYPE_CHECKING: from game import Game from game.coalition import Coalition from gen.flights.flight import FlightType + from game.theater import ControlPoint @dataclass @@ -73,6 +75,33 @@ class Pilot: return Pilot(faker.name()) +@dataclass(frozen=True) +class OperatingBases: + shore: bool + carrier: bool + lha: bool + + @classmethod + def default_for_aircraft(cls, aircraft: AircraftType) -> OperatingBases: + if aircraft.dcs_unit_type.helicopter: + # Helicopters operate from anywhere by default. + return OperatingBases(shore=True, carrier=True, lha=True) + if aircraft.lha_capable: + # Marine aircraft operate from LHAs and the shore by default. + return OperatingBases(shore=True, carrier=False, lha=True) + if aircraft.carrier_capable: + # Carrier aircraft operate from carriers by default. + return OperatingBases(shore=False, carrier=True, lha=False) + # And the rest are only capable of shore operation. + return OperatingBases(shore=True, carrier=False, lha=False) + + @classmethod + def from_yaml(cls, aircraft: AircraftType, data: dict[str, bool]) -> OperatingBases: + return dataclasses.replace( + OperatingBases.default_for_aircraft(aircraft), **data + ) + + @dataclass class Squadron: name: str @@ -82,6 +111,7 @@ class Squadron: aircraft: AircraftType livery: Optional[str] mission_types: tuple[FlightType, ...] + operating_bases: OperatingBases #: The pool of pilots that have not yet been assigned to the squadron. This only #: happens when a preset squadron defines more preset pilots than the squadron limit @@ -252,6 +282,14 @@ class Squadron: def can_auto_assign(self, task: FlightType) -> bool: return task in self.auto_assignable_mission_types + def operates_from(self, control_point: ControlPoint) -> bool: + if control_point.is_carrier: + return self.operating_bases.carrier + elif control_point.is_lha: + return self.operating_bases.lha + else: + return self.operating_bases.shore + def pilot_at_index(self, index: int) -> Pilot: return self.current_roster[index] @@ -290,6 +328,7 @@ class Squadron: aircraft=unit_type, livery=data.get("livery"), mission_types=tuple(mission_types), + operating_bases=OperatingBases.from_yaml(unit_type, data.get("bases", {})), pilot_pool=pilots, coalition=coalition, settings=game.settings, @@ -379,6 +418,7 @@ class AirWing: aircraft=aircraft, livery=None, mission_types=tuple(tasks_for_aircraft(aircraft)), + operating_bases=OperatingBases.default_for_aircraft(aircraft), pilot_pool=[], coalition=coalition, settings=game.settings,