diff --git a/changelog.md b/changelog.md index 581d048b..eb6d4fe8 100644 --- a/changelog.md +++ b/changelog.md @@ -5,8 +5,7 @@ * **[Flight Planner]** Added BAI missions. * **[Flight Planner]** Added anti-ship missions. * **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package. -* **[Modding]** Possible to setup liveries overrides for factions -* **[Units]** Added support for F-14A-135-GR +* **[Culling]** Added possibility to include/exclude carriers from culling zones # 2.2.1 @@ -15,13 +14,17 @@ * **[Factions]** Added map Persian Gulf full by Plob * **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately. * **[UI]** Mission start screen now informs players about delayed flights. +* **[Units]** Added support for F-14A-135-GR +* **[Modding]** Possible to setup liveries overrides in factions definition files ## Fixes : * **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned. -* **[Flight Planner]** Spitfire clipped wing variant was not seen as a flyable module * **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior. * **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error. * **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings. +* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot) +* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though) + # 2.2.0 diff --git a/game/db.py b/game/db.py index c73f7dbf..05eaad06 100644 --- a/game/db.py +++ b/game/db.py @@ -1244,7 +1244,7 @@ def unit_type_name_2(unit_type) -> str: return unit_type.name and unit_type.name or unit_type.id -def unit_type_from_name(name: str) -> Optional[UnitType]: +def unit_type_from_name(name: str) -> Optional[Type[UnitType]]: if name in vehicle_map: return vehicle_map[name] elif name in plane_map: diff --git a/game/event/event.py b/game/event/event.py index ea5f3b80..db8e5ee6 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -107,14 +107,16 @@ class Event: for destroyed_aircraft in debriefing.killed_aircrafts: try: cpid = int(destroyed_aircraft.split("|")[3]) - type = db.unit_type_from_name(destroyed_aircraft.split("|")[4]) - if cpid in cp_map.keys(): + aircraft = db.unit_type_from_name( + destroyed_aircraft.split("|")[4]) + if cpid in cp_map: cp = cp_map[cpid] - if type in cp.base.aircraft.keys(): - logging.info("Aircraft destroyed : " + str(type)) - cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1) - except Exception as e: - print(e) + if aircraft in cp.base.aircraft: + logging.info(f"Aircraft destroyed: {aircraft}") + cp.base.aircraft[aircraft] = max( + 0, cp.base.aircraft[aircraft] - 1) + except Exception: + logging.exception("Failed to commit destroyed aircraft") # ------------------------------ # Destroyed ground units @@ -123,13 +125,13 @@ class Event: for killed_ground_unit in debriefing.killed_ground_units: try: cpid = int(killed_ground_unit.split("|")[3]) - type = db.unit_type_from_name(killed_ground_unit.split("|")[4]) + aircraft = db.unit_type_from_name(killed_ground_unit.split("|")[4]) if cpid in cp_map.keys(): killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1 cp = cp_map[cpid] - if type in cp.base.armor.keys(): - logging.info("Ground unit destroyed : " + str(type)) - cp.base.armor[type] = max(0, cp.base.armor[type] - 1) + if aircraft in cp.base.armor.keys(): + logging.info("Ground unit destroyed : " + str(aircraft)) + cp.base.armor[aircraft] = max(0, cp.base.armor[aircraft] - 1) except Exception as e: print(e) @@ -352,11 +354,13 @@ class Event: logging.info(info.text) - class UnitsDeliveryEvent(Event): + informational = True - def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game): + def __init__(self, attacker_name: str, defender_name: str, + from_cp: ControlPoint, to_cp: ControlPoint, + game: Game) -> None: super(UnitsDeliveryEvent, self).__init__(game=game, location=to_cp.position, from_cp=from_cp, @@ -364,17 +368,16 @@ class UnitsDeliveryEvent(Event): attacker_name=attacker_name, defender_name=defender_name) - self.units: Dict[UnitType, int] = {} + self.units: Dict[Type[UnitType], int] = {} - def __str__(self): + def __str__(self) -> str: return "Pending delivery to {}".format(self.to_cp) - def deliver(self, units: Dict[UnitType, int]): + def deliver(self, units: Dict[Type[UnitType], int]) -> None: for k, v in units.items(): self.units[k] = self.units.get(k, 0) + v - def skip(self): - + def skip(self) -> None: for k, v in self.units.items(): info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn) self.game.informations.append(info) diff --git a/game/game.py b/game/game.py index 3b29c46c..8f691b52 100644 --- a/game/game.py +++ b/game/game.py @@ -84,7 +84,8 @@ class Game: self.ground_planners: Dict[int, GroundPlanner] = {} self.informations = [] self.informations.append(Information("Game Start", "-" * 40, 0)) - self.__culling_points = self.compute_conflicts_position() + self.__culling_points: List[Point] = [] + self.compute_conflicts_position() self.__destroyed_units: List[str] = [] self.savepath = "" self.budget = PLAYER_BUDGET_INITIAL @@ -239,7 +240,7 @@ class Game: self.aircraft_inventory.set_from_control_point(cp) # Plan flights & combat for next turn - self.__culling_points = self.compute_conflicts_position() + self.compute_conflicts_position() self.ground_planners = {} self.blue_ato.clear() self.red_ato.clear() @@ -316,7 +317,7 @@ class Game: if i > 50 or budget_for_aircraft <= 0: break target_cp = random.choice(potential_cp_armor) - if target_cp.base.total_planes >= MAX_AIRCRAFT: + if target_cp.base.total_aircraft >= MAX_AIRCRAFT: continue unit = random.choice(potential_units) price = db.PRICES[unit] * 2 @@ -364,6 +365,12 @@ class Game: points.append(front_line.control_point_a.position) points.append(front_line.control_point_b.position) + # If do_not_cull_carrier is enabled, add carriers as culling point + if self.settings.perf_do_not_cull_carrier: + for cp in self.theater.controlpoints: + if cp.is_carrier or cp.is_lha: + points.append(cp.position) + # If there is no conflict take the center point between the two nearest opposing bases if len(points) == 0: cpoint = None @@ -387,7 +394,7 @@ class Game: if len(points) == 0: points.append(Point(0, 0)) - return points + self.__culling_points = points def add_destroyed_units(self, data): pos = Point(data["x"], data["z"]) diff --git a/game/inventory.py b/game/inventory.py index 3c92a80f..80adb72b 100644 --- a/game/inventory.py +++ b/game/inventory.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING +from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type from dcs.unittype import FlyingType @@ -17,9 +17,9 @@ class ControlPointAircraftInventory: def __init__(self, control_point: ControlPoint) -> None: self.control_point = control_point - self.inventory: Dict[FlyingType, int] = defaultdict(int) + self.inventory: Dict[Type[FlyingType], int] = defaultdict(int) - def add_aircraft(self, aircraft: FlyingType, count: int) -> None: + def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None: """Adds aircraft to the inventory. Args: @@ -28,7 +28,7 @@ class ControlPointAircraftInventory: """ self.inventory[aircraft] += count - def remove_aircraft(self, aircraft: FlyingType, count: int) -> None: + def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None: """Removes aircraft from the inventory. Args: @@ -47,7 +47,7 @@ class ControlPointAircraftInventory: ) self.inventory[aircraft] -= count - def available(self, aircraft: FlyingType) -> int: + def available(self, aircraft: Type[FlyingType]) -> int: """Returns the number of available aircraft of the given type. Args: diff --git a/game/settings.py b/game/settings.py index ff73b63c..1e54c1b4 100644 --- a/game/settings.py +++ b/game/settings.py @@ -39,6 +39,7 @@ class Settings: # Performance culling perf_culling: bool = False perf_culling_distance: int = 100 + perf_do_not_cull_carrier = True # LUA Plugins system plugins: Dict[str, bool] = field(default_factory=dict) diff --git a/game/theater/base.py b/game/theater/base.py index 47b3580e..ba8a72f8 100644 --- a/game/theater/base.py +++ b/game/theater/base.py @@ -4,9 +4,8 @@ import math import typing from typing import Dict, Type -from dcs.planes import PlaneType from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task -from dcs.unittype import UnitType, VehicleType +from dcs.unittype import FlyingType, UnitType, VehicleType from dcs.vehicles import AirDefence, Armor from game import db @@ -21,20 +20,16 @@ BASE_MIN_STRENGTH = 0 class Base: - aircraft = {} # type: typing.Dict[PlaneType, int] - armor = {} # type: typing.Dict[VehicleType, int] - aa = {} # type: typing.Dict[AirDefence, int] - strength = 1 # type: float def __init__(self): - self.aircraft = {} - self.armor = {} - self.aa = {} + self.aircraft: Dict[Type[FlyingType], int] = {} + self.armor: Dict[VehicleType, int] = {} + self.aa: Dict[AirDefence, int] = {} self.commision_points: Dict[Type, float] = {} self.strength = 1 @property - def total_planes(self) -> int: + def total_aircraft(self) -> int: return sum(self.aircraft.values()) @property @@ -83,7 +78,7 @@ class Base: logging.info("{} for {} ({}): {}".format(self, for_type, count, result)) return result - def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]: + def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[FlyingType, int]: return self._find_best_unit(self.aircraft, for_type, count) def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]: @@ -155,7 +150,7 @@ class Base: if task: count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task]) else: - count = self.total_planes + count = self.total_aircraft count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength)) return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count) @@ -167,18 +162,18 @@ class Base: # previous logic removed because we always want the full air defense capabilities. return self.total_aa - def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]: + def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]: return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) def scramble_last_defense(self): # return as many CAP-capable aircraft as we can since this is the last defense of the base # (but not more than 20 - that's just nuts) - return self._find_best_planes(CAP, min(self.total_planes, 20)) + return self._find_best_planes(CAP, min(self.total_aircraft, 20)) - def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]: + def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]: return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS)) - def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]: + def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]: return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) def assemble_attack(self) -> typing.Dict[Armor, int]: diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index ca21d463..476f831a 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -235,7 +235,7 @@ class ControlPoint(MissionTarget): return result @property - def available_aircraft_slots(self): + def total_aircraft_parking(self): """ :return: The maximum number of aircraft that can be stored in this control point """ @@ -376,6 +376,19 @@ class ControlPoint(MissionTarget): return False return True + @property + def expected_aircraft_next_turn(self) -> int: + total = self.base.total_aircraft + assert self.pending_unit_deliveries + for unit_bought in self.pending_unit_deliveries.units: + if issubclass(unit_bought, FlyingType): + total += self.pending_unit_deliveries.units[unit_bought] + return total + + @property + def unclaimed_parking(self) -> int: + return self.total_aircraft_parking - self.expected_aircraft_next_turn + class OffMapSpawn(ControlPoint): def __init__(self, id: int, name: str, position: Point): @@ -391,5 +404,5 @@ class OffMapSpawn(ControlPoint): yield from [] @property - def available_aircraft_slots(self) -> int: + def total_aircraft_parking(self) -> int: return 1000 diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 56d9d04a..276a6396 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta from enum import Enum -from typing import Dict, List, Optional, TYPE_CHECKING +from typing import Dict, List, Optional, TYPE_CHECKING, Type from dcs.mapping import Point from dcs.point import MovingPoint, PointAction @@ -133,8 +133,8 @@ class FlightWaypoint: class Flight: - def __init__(self, package: Package, unit_type: FlyingType, count: int, - flight_type: FlightType, start_type: str, + def __init__(self, package: Package, unit_type: Type[FlyingType], + count: int, flight_type: FlightType, start_type: str, departure: ControlPoint, arrival: ControlPoint, divert: Optional[ControlPoint]) -> None: self.package = package diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 50fc5fba..0937f8c9 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -296,7 +296,7 @@ class QLiberationMap(QGraphicsView): # Display Culling if DisplayOptions.culling and self.game.settings.perf_culling: - self.display_culling() + self.display_culling(scene) for cp in self.game.theater.controlpoints: diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index f9f7c159..3740448a 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -57,7 +57,7 @@ class QBaseMenu2(QDialog): title = QLabel("" + self.cp.name + "") title.setAlignment(Qt.AlignLeft | Qt.AlignTop) title.setProperty("style", "base-title") - unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_planes, self.cp.base.total_armor, + unitsPower = QLabel("{} / {} / Runway : {}".format(self.cp.base.total_aircraft, self.cp.base.total_armor, "Available" if self.cp.has_runway() else "Unavailable")) self.topLayout.addWidget(title) self.topLayout.addWidget(unitsPower) diff --git a/qt_ui/windows/basemenu/QRecruitBehaviour.py b/qt_ui/windows/basemenu/QRecruitBehaviour.py index 5cb26a81..7f462c57 100644 --- a/qt_ui/windows/basemenu/QRecruitBehaviour.py +++ b/qt_ui/windows/basemenu/QRecruitBehaviour.py @@ -1,4 +1,5 @@ import logging +from typing import Type from PySide2.QtWidgets import ( QGroupBox, @@ -106,7 +107,7 @@ class QRecruitBehaviour: return row + 1 - def _update_count_label(self, unit_type: UnitType): + def _update_count_label(self, unit_type: Type[UnitType]): self.bought_amount_labels[unit_type].setText("{}".format( unit_type in self.pending_deliveries.units and "{}".format(self.pending_deliveries.units[unit_type]) or "0" @@ -125,14 +126,7 @@ class QRecruitBehaviour: child.setText( QRecruitBehaviour.BUDGET_FORMAT.format(self.budget)) - def buy(self, unit_type): - - if self.maximum_units > 0: - if self.total_units + 1 > self.maximum_units: - logging.info("Not enough space left !") - # TODO : display modal warning - return - + def buy(self, unit_type: Type[UnitType]): price = db.PRICES[unit_type] if self.budget >= price: self.pending_deliveries.deliver({unit_type: 1}) @@ -158,19 +152,6 @@ class QRecruitBehaviour: self._update_count_label(unit_type) self.update_available_budget() - @property - def total_units(self): - total = 0 - for unit_type in self.recruitables_types: - total += self.cp.base.total_units(unit_type) - - if self.pending_deliveries: - for unit_bought in self.pending_deliveries.units: - if db.unit_task(unit_bought) in self.recruitables_types: - total += self.pending_deliveries.units[unit_bought] - - return total - def set_maximum_units(self, maximum_units): """ Set the maximum number of units that can be bought diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 3e8fef9f..7c159e94 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -1,3 +1,4 @@ +import logging from typing import Optional, Set from PySide2.QtCore import Qt @@ -31,13 +32,13 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): self.existing_units_labels = {} # Determine maximum number of aircrafts that can be bought - self.set_maximum_units(self.cp.available_aircraft_slots) + self.set_maximum_units(self.cp.total_aircraft_parking) self.set_recruitable_types([CAP, CAS]) self.bought_amount_labels = {} self.existing_units_labels = {} - self.hangar_status = QHangarStatus(self.total_units, self.cp.available_aircraft_slots) + self.hangar_status = QHangarStatus(self.cp) self.init_ui() @@ -80,8 +81,13 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): self.setLayout(main_layout) def buy(self, unit_type): + if self.maximum_units > 0: + if self.cp.unclaimed_parking <= 0: + logging.debug(f"No space for additional aircraft at {self.cp}.") + return + super().buy(unit_type) - self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots) + self.hangar_status.update_label() def sell(self, unit_type: UnitType): # Don't need to remove aircraft from the inventory if we're canceling @@ -99,22 +105,26 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour): "assigned to a mission?", QMessageBox.Ok) return super().sell(unit_type) - self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots) + self.hangar_status.update_label() class QHangarStatus(QHBoxLayout): - def __init__(self, current_amount: int, max_amount: int): - super(QHangarStatus, self).__init__() + def __init__(self, control_point: ControlPoint) -> None: + super().__init__() + self.control_point = control_point + self.icon = QLabel() self.icon.setPixmap(ICONS["Hangar"]) self.text = QLabel("") - self.update_label(current_amount, max_amount) + self.update_label() self.addWidget(self.icon, Qt.AlignLeft) self.addWidget(self.text, Qt.AlignLeft) self.addStretch(50) self.setAlignment(Qt.AlignLeft) - def update_label(self, current_amount: int, max_amount: int): - self.text.setText("{}/{}".format(current_amount, max_amount)) + def update_label(self) -> None: + current_amount = self.control_point.expected_aircraft_next_turn + max_amount = self.control_point.total_aircraft_parking + self.text.setText(f"{current_amount}/{max_amount}") diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index 486068f5..1506187d 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -279,6 +279,10 @@ class QSettingsWindow(QDialog): self.culling_distance.setValue(self.game.settings.perf_culling_distance) self.culling_distance.valueChanged.connect(self.applySettings) + self.culling_do_not_cull_carrier = QCheckBox() + self.culling_do_not_cull_carrier.setChecked(self.game.settings.perf_do_not_cull_carrier) + self.culling_do_not_cull_carrier.toggled.connect(self.applySettings) + self.performanceLayout.addWidget(QLabel("Smoke visual effect on frontline"), 0, 0) self.performanceLayout.addWidget(self.smoke, 0, 1, alignment=Qt.AlignRight) self.performanceLayout.addWidget(QLabel("SAM starts in RED alert mode"), 1, 0) @@ -299,6 +303,8 @@ class QSettingsWindow(QDialog): self.performanceLayout.addWidget(self.culling, 8, 1, alignment=Qt.AlignRight) self.performanceLayout.addWidget(QLabel("Culling distance (km)"), 9, 0) self.performanceLayout.addWidget(self.culling_distance, 9, 1, alignment=Qt.AlignRight) + self.performanceLayout.addWidget(QLabel("Do not cull carrier's surroundings"), 10, 0) + self.performanceLayout.addWidget(self.culling_do_not_cull_carrier, 10, 1, alignment=Qt.AlignRight) self.generatorLayout.addWidget(self.gameplay) self.generatorLayout.addWidget(QLabel("Disabling settings below may improve performance, but will impact the overall quality of the experience.")) @@ -366,9 +372,11 @@ class QSettingsWindow(QDialog): self.game.settings.perf_culling = self.culling.isChecked() self.game.settings.perf_culling_distance = int(self.culling_distance.value()) + self.game.settings.perf_do_not_cull_carrier = self.culling_do_not_cull_carrier.isChecked() self.game.settings.show_red_ato = self.cheat_options.show_red_ato + self.game.compute_conflicts_position() GameUpdateSignal.get_instance().updateGame(self.game) def onSelectionChanged(self):