From 2bd673a53185d8cb39f68c5fa29da1e1642f3fd7 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 25 Nov 2020 18:48:07 -0800 Subject: [PATCH] Don't allow operating on broken runways. Doesn't allow helos or harriers to do it either even though they should be able to because we don't currently support ground spawns, which would be needed to prevent those aircraft from using the runway. Even then, I don't know if they can be forced to *land* vertically. Fixes https://github.com/Khopa/dcs_liberation/issues/432 --- game/db.py | 2 +- game/inventory.py | 10 ++++--- game/theater/controlpoint.py | 29 +++++++++++-------- gen/flights/ai_flight_planner.py | 6 ++-- qt_ui/widgets/combos/QAircraftTypeSelector.py | 4 +-- .../combos/QArrivalAirfieldSelector.py | 7 ++--- .../widgets/combos/QOriginAirfieldSelector.py | 8 +++-- qt_ui/widgets/map/QMapControlPoint.py | 2 +- qt_ui/windows/basemenu/QBaseMenuTabs.py | 5 ++-- qt_ui/windows/basemenu/QRecruitBehaviour.py | 6 +++- .../airfield/QAircraftRecruitmentMenu.py | 12 +++++--- 11 files changed, 54 insertions(+), 37 deletions(-) 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)