diff --git a/game/db.py b/game/db.py index 5f0bd302..fd731dad 100644 --- a/game/db.py +++ b/game/db.py @@ -1223,7 +1223,7 @@ def unit_task(unit: UnitType) -> Optional[Task]: return None -def find_unittype(for_task: Task, country_name: str) -> List[UnitType]: +def find_unittype(for_task: Task, country_name: str) -> List[Type[UnitType]]: return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name].units] diff --git a/game/inventory.py b/game/inventory.py index 80adb72b..b369ad8b 100644 --- a/game/inventory.py +++ b/game/inventory.py @@ -59,14 +59,14 @@ class ControlPointAircraftInventory: return 0 @property - def types_available(self) -> Iterator[FlyingType]: + def types_available(self) -> Iterator[Type[FlyingType]]: """Iterates over all available aircraft types.""" for aircraft, count in self.inventory.items(): if count > 0: yield aircraft @property - def all_aircraft(self) -> Iterator[Tuple[FlyingType, int]]: + def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]: """Iterates over all available aircraft types, including amounts.""" for aircraft, count in self.inventory.items(): if count > 0: @@ -106,12 +106,14 @@ class GlobalAircraftInventory: return self.inventories[control_point] @property - def available_types_for_player(self) -> Iterator[FlyingType]: + def available_types_for_player(self) -> Iterator[Type[FlyingType]]: """Iterates over all aircraft types available to the player.""" - seen: Set[FlyingType] = set() + seen: Set[Type[FlyingType]] = set() for control_point, inventory in self.inventories.items(): if control_point.captured: for aircraft in inventory.types_available: + if not control_point.can_operate(aircraft): + continue if aircraft not in seen: seen.add(aircraft) yield aircraft diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index c2549f91..ef689c03 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -7,7 +7,7 @@ import re from abc import ABC, abstractmethod from dataclasses import dataclass, field from enum import Enum -from typing import Dict, Iterator, List, Optional, TYPE_CHECKING +from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type from dcs.mapping import Point from dcs.ships import ( @@ -283,7 +283,7 @@ class ControlPoint(MissionTarget, ABC): self.stances[to.id] = CombatStance.DEFENSIVE @abstractmethod - def has_runway(self) -> bool: + def runway_is_operational(self) -> bool: """ Check whether this control point supports taking offs and landings. :return: @@ -363,7 +363,7 @@ class ControlPoint(MissionTarget, ABC): BaseDefenseGenerator(game, self).generate() @abstractmethod - def can_land(self, aircraft: FlyingType) -> bool: + def can_operate(self, aircraft: Type[FlyingType]) -> bool: ... def aircraft_transferring(self, game: Game) -> int: @@ -437,8 +437,13 @@ class Airfield(ControlPoint): self.airport = airport self._runway_status = RunwayStatus() - def can_land(self, aircraft: FlyingType) -> bool: - return True + def can_operate(self, aircraft: FlyingType) -> bool: + # TODO: Allow helicopters. + # Need to implement ground spawns so the helos don't use the runway. + # TODO: Allow harrier. + # Needs ground spawns just like helos do, but also need to be able to + # limit takeoff weight to ~20500 lbs or it won't be able to take off. + return self.runway_is_operational() def mission_types(self, for_player: bool) -> Iterator[FlightType]: from gen.flights.flight import FlightType @@ -462,7 +467,7 @@ class Airfield(ControlPoint): def heading(self) -> int: return self.airport.runways[0].heading - def has_runway(self) -> bool: + def runway_is_operational(self) -> bool: return not self.runway_status.damaged @property @@ -503,7 +508,7 @@ class NavalControlPoint(ControlPoint, ABC): def heading(self) -> int: return 0 # TODO compute heading - def has_runway(self) -> bool: + def runway_is_operational(self) -> bool: # Necessary because it's possible for the carrier itself to have sunk # while its escorts are still alive. for g in self.ground_objects: @@ -525,7 +530,7 @@ class NavalControlPoint(ControlPoint, ABC): @property def runway_status(self) -> RunwayStatus: - return RunwayStatus(damaged=not self.has_runway()) + return RunwayStatus(damaged=not self.runway_is_operational()) @property def runway_can_be_repaired(self) -> bool: @@ -548,7 +553,7 @@ class Carrier(NavalControlPoint): def is_carrier(self): return True - def can_land(self, aircraft: FlyingType) -> bool: + def can_operate(self, aircraft: FlyingType) -> bool: return aircraft in db.CARRIER_CAPABLE @property @@ -571,7 +576,7 @@ class Lha(NavalControlPoint): def is_lha(self) -> bool: return True - def can_land(self, aircraft: FlyingType) -> bool: + def can_operate(self, aircraft: FlyingType) -> bool: return aircraft in db.LHA_CAPABLE @property @@ -581,7 +586,7 @@ class Lha(NavalControlPoint): class OffMapSpawn(ControlPoint): - def has_runway(self) -> bool: + def runway_is_operational(self) -> bool: return True def __init__(self, cp_id: int, name: str, position: Point): @@ -600,7 +605,7 @@ class OffMapSpawn(ControlPoint): def total_aircraft_parking(self) -> int: return 1000 - def can_land(self, aircraft: FlyingType) -> bool: + def can_operate(self, aircraft: FlyingType) -> bool: return True @property diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 37cf0844..4dc0944e 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -201,7 +201,7 @@ class AircraftAllocator: def find_aircraft_of_type( self, flight: ProposedFlight, types: List[Type[FlyingType]], - ) -> Optional[Tuple[ControlPoint, FlyingType]]: + ) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]: airfields_in_range = self.closest_airfields.airfields_within( flight.max_distance ) @@ -210,6 +210,8 @@ class AircraftAllocator: continue inventory = self.global_inventory.for_control_point(airfield) for aircraft, available in inventory.all_aircraft: + if not airfield.can_operate(aircraft): + continue if aircraft in types and available >= flight.num_aircraft: inventory.remove_aircraft(aircraft, flight.num_aircraft) return airfield, aircraft @@ -264,7 +266,7 @@ class PackageBuilder: continue if airfield == arrival: continue - if not airfield.can_land(aircraft): + if not airfield.can_operate(aircraft): continue if isinstance(airfield, OffMapSpawn): continue diff --git a/qt_ui/widgets/combos/QAircraftTypeSelector.py b/qt_ui/widgets/combos/QAircraftTypeSelector.py index 2be6e48c..f31c611d 100644 --- a/qt_ui/widgets/combos/QAircraftTypeSelector.py +++ b/qt_ui/widgets/combos/QAircraftTypeSelector.py @@ -1,5 +1,5 @@ """Combo box for selecting aircraft types.""" -from typing import Iterable +from typing import Iterable, Type from PySide2.QtWidgets import QComboBox @@ -9,7 +9,7 @@ from dcs.unittype import FlyingType class QAircraftTypeSelector(QComboBox): """Combo box for selecting among the given aircraft types.""" - def __init__(self, aircraft_types: Iterable[FlyingType]) -> None: + def __init__(self, aircraft_types: Iterable[Type[FlyingType]]) -> None: super().__init__() for aircraft in aircraft_types: self.addItem(f"{aircraft.id}", userData=aircraft) diff --git a/qt_ui/widgets/combos/QArrivalAirfieldSelector.py b/qt_ui/widgets/combos/QArrivalAirfieldSelector.py index b495255b..22097b34 100644 --- a/qt_ui/widgets/combos/QArrivalAirfieldSelector.py +++ b/qt_ui/widgets/combos/QArrivalAirfieldSelector.py @@ -1,10 +1,9 @@ """Combo box for selecting a departure airfield.""" -from typing import Iterable +from typing import Iterable, Type from PySide2.QtWidgets import QComboBox from dcs.unittype import FlyingType -from game import db from game.theater.controlpoint import ControlPoint @@ -16,7 +15,7 @@ class QArrivalAirfieldSelector(QComboBox): """ def __init__(self, destinations: Iterable[ControlPoint], - aircraft: FlyingType, optional_text: str) -> None: + aircraft: Type[FlyingType], optional_text: str) -> None: super().__init__() self.destinations = list(destinations) self.aircraft = aircraft @@ -33,7 +32,7 @@ class QArrivalAirfieldSelector(QComboBox): def rebuild_selector(self) -> None: self.clear() for destination in self.destinations: - if destination.can_land(self.aircraft): + if destination.can_operate(self.aircraft): self.addItem(destination.name, destination) self.model().sort(0) self.insertItem(0, self.optional_text, None) diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py index ce1c6301..5a91a74d 100644 --- a/qt_ui/widgets/combos/QOriginAirfieldSelector.py +++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py @@ -1,5 +1,5 @@ """Combo box for selecting a departure airfield.""" -from typing import Iterable +from typing import Iterable, Type from PySide2.QtCore import Signal from PySide2.QtWidgets import QComboBox @@ -20,7 +20,7 @@ class QOriginAirfieldSelector(QComboBox): def __init__(self, global_inventory: GlobalAircraftInventory, origins: Iterable[ControlPoint], - aircraft: FlyingType) -> None: + aircraft: Type[FlyingType]) -> None: super().__init__() self.global_inventory = global_inventory self.origins = list(origins) @@ -37,12 +37,14 @@ class QOriginAirfieldSelector(QComboBox): def rebuild_selector(self) -> None: self.clear() for origin in self.origins: + if not origin.can_operate(self.aircraft): + continue + inventory = self.global_inventory.for_control_point(origin) available = inventory.available(self.aircraft) if available: self.addItem(f"{origin.name} ({available} available)", origin) self.model().sort(0) - self.update() @property def available(self) -> int: diff --git a/qt_ui/widgets/map/QMapControlPoint.py b/qt_ui/widgets/map/QMapControlPoint.py index 45f78860..ef2cc0ca 100644 --- a/qt_ui/widgets/map/QMapControlPoint.py +++ b/qt_ui/widgets/map/QMapControlPoint.py @@ -33,7 +33,7 @@ class QMapControlPoint(QMapObject): painter.setBrush(self.brush_color) painter.setPen(self.pen_color) - if not self.control_point.has_runway(): + if not self.control_point.runway_is_operational(): painter.setBrush(const.COLORS["black"]) painter.setPen(self.brush_color) diff --git a/qt_ui/windows/basemenu/QBaseMenuTabs.py b/qt_ui/windows/basemenu/QBaseMenuTabs.py index 1e705372..b7bb551a 100644 --- a/qt_ui/windows/basemenu/QBaseMenuTabs.py +++ b/qt_ui/windows/basemenu/QBaseMenuTabs.py @@ -20,9 +20,8 @@ class QBaseMenuTabs(QTabWidget): self.intel = QIntelInfo(cp, game_model.game) self.addTab(self.intel, "Intel") else: - if cp.has_runway(): - self.airfield_command = QAirfieldCommand(cp, game_model) - self.addTab(self.airfield_command, "Airfield Command") + self.airfield_command = QAirfieldCommand(cp, game_model) + self.addTab(self.airfield_command, "Airfield Command") if cp.is_carrier: self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game) diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py index 7f462c57..3d4ac38c 100644 --- a/qt_ui/windows/basemenu/QRecruitBehaviour.py +++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py @@ -5,6 +5,7 @@ from PySide2.QtWidgets import ( QGroupBox, QHBoxLayout, QLabel, + QLayout, QPushButton, QSizePolicy, QSpacerItem, @@ -45,7 +46,8 @@ class QRecruitBehaviour: def budget(self, value: int) -> None: self.game_model.game.budget = value - def add_purchase_row(self, unit_type, layout, row): + def add_purchase_row(self, unit_type: Type[UnitType], layout: QLayout, + row: int, disabled: bool = False) -> int: exist = QGroupBox() exist.setProperty("style", "buy-box") exist.setMaximumHeight(36) @@ -80,6 +82,7 @@ class QRecruitBehaviour: buy = QPushButton("+") buy.setProperty("style", "btn-buy") + buy.setDisabled(disabled) buy.setMinimumSize(16, 16) buy.setMaximumSize(16, 16) buy.clicked.connect(lambda: self.buy(unit_type)) @@ -87,6 +90,7 @@ class QRecruitBehaviour: sell = QPushButton("-") sell.setProperty("style", "btn-sell") + sell.setDisabled(disabled) sell.setMinimumSize(16, 16) sell.setMaximumSize(16, 16) sell.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 159aa93a..82c7033d 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 +from typing import Optional, Set, Type from PySide2.QtCore import Qt from PySide2.QtWidgets import ( @@ -13,7 +13,7 @@ from PySide2.QtWidgets import ( QWidget, ) from dcs.task import CAP, CAS -from dcs.unittype import UnitType +from dcs.unittype import FlyingType, UnitType from game import db from game.theater import ControlPoint @@ -51,12 +51,14 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): task_box_layout = QGridLayout() row = 0 - unit_types: Set[UnitType] = set() + unit_types: Set[Type[FlyingType]] = set() for task in tasks: units = db.find_unittype(task, self.game_model.game.player_name) if not units: continue for unit in units: + if not issubclass(unit, FlyingType): + continue if self.cp.is_carrier and unit not in db.CARRIER_CAPABLE: continue if self.cp.is_lha and unit not in db.LHA_CAPABLE: @@ -65,7 +67,9 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): sorted_units = sorted(unit_types, key=lambda u: db.unit_type_name_2(u)) for unit_type in sorted_units: - row = self.add_purchase_row(unit_type, task_box_layout, row) + row = self.add_purchase_row( + unit_type, task_box_layout, row, + disabled=not self.cp.can_operate(unit_type)) stretch = QVBoxLayout() stretch.addStretch() task_box_layout.addLayout(stretch, row, 0)