diff --git a/changelog.md b/changelog.md index 2116f320..cadbc6c4 100644 --- a/changelog.md +++ b/changelog.md @@ -29,6 +29,7 @@ Saves from 2.3 are not compatible with 2.4. * **[UI]** Aircraft for a new flight are now only selectable if they match the task type for that flight. * **[UI]** WIP - There is now a unit info button for each unit in the recruitment list, that should help newer players learn what each unit does. * **[UI]** Docs for time-on-target and creating new theaters/factions/loadouts are now linked in the UI at the appropriate places. +* **[UI]** ASAP is now a checkbox rather than a button. Enabling this will disable the TOT selector but changes to the package structure will automatically re-ASAP the package. * **[Factions]** Added option for date-based loadout restriction. Active radar homing missiles are handled, patches welcome for the other thousand weapons. * **[Factions]** Added Poland 2010 faction. * **[Factions]** Added Greece 2005 faction. diff --git a/gen/ato.py b/gen/ato.py index fd7da75f..99a76789 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -20,6 +20,7 @@ from game.theater.missiontarget import MissionTarget from game.utils import Speed from .flights.flight import Flight, FlightType from .flights.flightplan import FormationFlightPlan +from .flights.traveltime import TotEstimator @dataclass(frozen=True) @@ -54,6 +55,11 @@ class Package: delay: int = field(default=0) + #: True if the package ToT should be reset to ASAP whenever the player makes + #: a change. This is really a UI property rather than a game property, but + #: we want it to persist in the save. + auto_asap: bool = field(default=False) + #: Desired TOT as an offset from mission start. time_over_target: timedelta = field(default=timedelta()) @@ -127,6 +133,9 @@ class Package: return max(times) return None + def set_tot_asap(self) -> None: + self.time_over_target = TotEstimator(self).earliest_tot() + def add_flight(self, flight: Flight) -> None: """Adds a flight to the package.""" self.flights.append(flight) diff --git a/qt_ui/dialogs.py b/qt_ui/dialogs.py index 263bfb62..09fd2b93 100644 --- a/qt_ui/dialogs.py +++ b/qt_ui/dialogs.py @@ -60,7 +60,7 @@ class Dialog: """Opens the dialog to edit the given flight.""" cls.edit_flight_dialog = QEditFlightDialog( cls.game_model, - package_model.package, + package_model, flight, parent=parent ) diff --git a/qt_ui/models.py b/qt_ui/models.py index bd0d6397..7155b3e0 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -100,6 +100,8 @@ class PackageModel(QAbstractListModel): #: Emitted when this package is being deleted from the ATO. deleted = Signal() + tot_changed = Signal() + def __init__(self, package: Package) -> None: super().__init__() self.package = package @@ -139,6 +141,8 @@ class PackageModel(QAbstractListModel): """Adds the given flight to the package.""" self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) self.package.add_flight(flight) + # update_tot is not called here because the new flight does not have a + # flight plan yet. Will be called manually by the caller. self.endInsertRows() def delete_flight_at_index(self, index: QModelIndex) -> None: @@ -155,14 +159,24 @@ class PackageModel(QAbstractListModel): self.beginRemoveRows(QModelIndex(), index, index) self.package.remove_flight(flight) self.endRemoveRows() + self.update_tot() def flight_at_index(self, index: QModelIndex) -> Flight: """Returns the flight located at the given index.""" return self.package.flights[index.row()] - def update_tot(self, tot: datetime.timedelta) -> None: + def set_tot(self, tot: datetime.timedelta) -> None: self.package.time_over_target = tot - self.layoutChanged.emit() + self.update_tot() + + def set_asap(self, asap: bool) -> None: + self.package.auto_asap = asap + self.update_tot() + + def update_tot(self) -> None: + if self.package.auto_asap: + self.package.set_tot_asap() + self.tot_changed.emit() @property def mission_target(self) -> MissionTarget: diff --git a/qt_ui/windows/mission/QEditFlightDialog.py b/qt_ui/windows/mission/QEditFlightDialog.py index a3d8cbd2..a6893a47 100644 --- a/qt_ui/windows/mission/QEditFlightDialog.py +++ b/qt_ui/windows/mission/QEditFlightDialog.py @@ -4,9 +4,8 @@ from PySide2.QtWidgets import ( QVBoxLayout, ) -from gen.ato import Package from gen.flights.flight import Flight -from qt_ui.models import GameModel +from qt_ui.models import GameModel, PackageModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner @@ -15,7 +14,8 @@ from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner class QEditFlightDialog(QDialog): """Dialog window for editing flight plans and loadouts.""" - def __init__(self, game_model: GameModel, package: Package, flight: Flight, parent=None) -> None: + def __init__(self, game_model: GameModel, package_model: PackageModel, + flight: Flight, parent=None) -> None: super().__init__(parent=parent) self.game_model = game_model @@ -25,7 +25,8 @@ class QEditFlightDialog(QDialog): layout = QVBoxLayout() - self.flight_planner = QFlightPlanner(package, flight, game_model.game) + self.flight_planner = QFlightPlanner(package_model, flight, + game_model.game) layout.addWidget(self.flight_planner) self.setLayout(layout) diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py index 2e78ce6e..10acc4fe 100644 --- a/qt_ui/windows/mission/QPackageDialog.py +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -3,8 +3,9 @@ import logging from datetime import timedelta from typing import Optional -from PySide2.QtCore import QItemSelection, QTime, Signal, Qt +from PySide2.QtCore import QItemSelection, QTime, Qt, Signal from PySide2.QtWidgets import ( + QCheckBox, QDialog, QHBoxLayout, QLabel, @@ -15,16 +16,15 @@ from PySide2.QtWidgets import ( ) from game.game import Game +from game.theater.missiontarget import MissionTarget from gen.ato import Package from gen.flights.flight import Flight from gen.flights.flightplan import FlightPlanBuilder, PlanningError -from gen.flights.traveltime import TotEstimator from qt_ui.models import AtoModel, GameModel, PackageModel from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.ato import QFlightList from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator -from game.theater.missiontarget import MissionTarget class QPackageDialog(QDialog): @@ -79,15 +79,17 @@ class QPackageDialog(QDialog): self.tot_spinner.setDisplayFormat("T+hh:mm:ss") self.tot_spinner.timeChanged.connect(self.save_tot) self.tot_spinner.setToolTip("Package TOT relative to mission TOT") + self.tot_spinner.setEnabled(not self.package_model.package.auto_asap) self.tot_column.addWidget(self.tot_spinner) - self.reset_tot_button = QPushButton("ASAP") - self.reset_tot_button.setToolTip( + self.auto_asap = QCheckBox("ASAP") + self.auto_asap.setToolTip( "Sets the package TOT to the earliest time that all flights can " "arrive at the target." ) - self.reset_tot_button.clicked.connect(self.reset_tot) - self.tot_column.addWidget(self.reset_tot_button) + self.auto_asap.setChecked(self.package_model.package.auto_asap) + self.auto_asap.toggled.connect(self.set_asap) + self.tot_column.addWidget(self.auto_asap) self.tot_help_label = QLabel("Help") self.tot_help_label.setAlignment(Qt.AlignCenter) @@ -113,6 +115,8 @@ class QPackageDialog(QDialog): self.delete_flight_button.setEnabled(model.rowCount() > 0) self.button_layout.addWidget(self.delete_flight_button) + self.package_model.tot_changed.connect(self.update_tot) + self.button_layout.addStretch() self.setLayout(self.layout) @@ -145,14 +149,14 @@ class QPackageDialog(QDialog): def save_tot(self) -> None: time = self.tot_spinner.time() seconds = time.hour() * 3600 + time.minute() * 60 + time.second() - self.package_model.update_tot(timedelta(seconds=seconds)) + self.package_model.set_tot(timedelta(seconds=seconds)) - def reset_tot(self) -> None: - if not list(self.package_model.flights): - self.package_model.update_tot(timedelta()) - else: - self.package_model.update_tot( - TotEstimator(self.package_model.package).earliest_tot()) + def set_asap(self, checked: bool) -> None: + self.package_model.set_asap(checked) + self.tot_spinner.setEnabled(not self.package_model.package.auto_asap) + self.update_tot() + + def update_tot(self) -> None: self.tot_spinner.setTime(self.tot_qtime()) def on_selection_changed(self, selected: QItemSelection, @@ -183,6 +187,7 @@ class QPackageDialog(QDialog): QMessageBox.critical( self, "Could not create flight", str(ex), QMessageBox.Ok ) + self.package_model.update_tot() # noinspection PyUnresolvedReferences self.package_changed.emit() diff --git a/qt_ui/windows/mission/flight/QFlightPlanner.py b/qt_ui/windows/mission/flight/QFlightPlanner.py index b4eb9b36..71904b48 100644 --- a/qt_ui/windows/mission/flight/QFlightPlanner.py +++ b/qt_ui/windows/mission/flight/QFlightPlanner.py @@ -1,9 +1,8 @@ -from PySide2.QtCore import Signal from PySide2.QtWidgets import QTabWidget from game import Game -from gen.ato import Package from gen.flights.flight import Flight +from qt_ui.models import PackageModel from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \ QFlightPayloadTab from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import \ @@ -14,22 +13,15 @@ from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import \ class QFlightPlanner(QTabWidget): - on_planned_flight_changed = Signal() - - def __init__(self, package: Package, flight: Flight, game: Game): + def __init__(self, package_model: PackageModel, flight: Flight, game: Game): super().__init__() self.general_settings_tab = QGeneralFlightSettingsTab( - game, package, flight + game, package_model, flight ) - # noinspection PyUnresolvedReferences - self.general_settings_tab.on_flight_settings_changed.connect( - lambda: self.on_planned_flight_changed.emit()) self.payload_tab = QFlightPayloadTab(flight, game) - self.waypoint_tab = QFlightWaypointTab(game, package, flight) - # noinspection PyUnresolvedReferences - self.waypoint_tab.on_flight_changed.connect( - lambda: self.on_planned_flight_changed.emit()) + self.waypoint_tab = QFlightWaypointTab(game, package_model.package, + flight) self.addTab(self.general_settings_tab, "General Flight settings") self.addTab(self.payload_tab, "Payload") self.addTab(self.waypoint_tab, "Waypoints") diff --git a/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py b/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py index 6d789585..fe1271ca 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py +++ b/qt_ui/windows/mission/flight/settings/QFlightDepartureDisplay.py @@ -1,32 +1,38 @@ -import datetime - from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout -from gen.ato import Package from gen.flights.flight import Flight from gen.flights.traveltime import TotEstimator -# TODO: Remove? +from qt_ui.models import PackageModel + + class QFlightDepartureDisplay(QGroupBox): - def __init__(self, package: Package, flight: Flight): + def __init__(self, package_model: PackageModel, flight: Flight): super().__init__("Departure") + self.package_model = package_model + self.flight = flight layout = QVBoxLayout() departure_row = QHBoxLayout() layout.addLayout(departure_row) - estimator = TotEstimator(package) - delay = estimator.mission_start_time(flight) - departure_row.addWidget(QLabel( f"Departing from {flight.from_cp.name}" )) - departure_row.addWidget(QLabel(f"At T+{delay}")) + self.departure_time = QLabel() + self.package_model.tot_changed.connect(self.update_departure_time) + departure_row.addWidget(self.departure_time) + self.update_departure_time() layout.addWidget(QLabel("Determined based on the package TOT. Edit the " "package to adjust the TOT.")) self.setLayout(layout) + + def update_departure_time(self) -> None: + estimator = TotEstimator(self.package_model.package) + delay = estimator.mission_start_time(self.flight) + self.departure_time.setText(f"At T+{delay}") diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index d6e745e0..1ecb9a28 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -1,15 +1,20 @@ import logging from PySide2.QtCore import Signal -from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout +from PySide2.QtWidgets import QLabel, QGroupBox, QSpinBox, QGridLayout + +from game import Game +from gen.flights.flight import Flight +from qt_ui.models import PackageModel class QFlightSlotEditor(QGroupBox): changed = Signal() - def __init__(self, flight, game): - super(QFlightSlotEditor, self).__init__("Slots") + def __init__(self, package_model: PackageModel, flight: Flight, game: Game): + super().__init__("Slots") + self.package_model = package_model self.flight = flight self.game = game self.inventory = self.game.aircraft_inventory.for_control_point( @@ -70,6 +75,7 @@ class QFlightSlotEditor(QGroupBox): def _changed_client_count(self): self.flight.client_count = int(self.client_count_spinner.value()) self._cap_client_count() + self.package_model.update_tot() self.changed.emit() def _cap_client_count(self): diff --git a/qt_ui/windows/mission/flight/settings/QFlightStartType.py b/qt_ui/windows/mission/flight/settings/QFlightStartType.py index 438c56de..b49fb5d7 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightStartType.py +++ b/qt_ui/windows/mission/flight/settings/QFlightStartType.py @@ -1,14 +1,14 @@ -from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QComboBox, QLabel -from dcs.mission import StartType +from PySide2.QtWidgets import QComboBox, QGroupBox, QHBoxLayout, QLabel from gen.flights.flight import Flight +from qt_ui.models import PackageModel class QFlightStartType(QGroupBox): - def __init__(self, flight:Flight): - super(QFlightStartType, self).__init__() - + def __init__(self, package_model: PackageModel, flight: Flight): + super().__init__() + self.package_model = package_model self.flight = flight self.layout = QHBoxLayout() @@ -28,6 +28,4 @@ class QFlightStartType(QGroupBox): def _on_start_type_selected(self): selected = self.start_type.currentData() self.flight.start_type = selected - - - + self.package_model.update_tot() \ No newline at end of file diff --git a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py index 71dfc036..5875a79d 100644 --- a/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py +++ b/qt_ui/windows/mission/flight/settings/QGeneralFlightSettingsTab.py @@ -1,9 +1,10 @@ from PySide2.QtCore import Signal -from PySide2.QtWidgets import QFrame, QGridLayout, QVBoxLayout, QLabel +from PySide2.QtWidgets import QFrame, QGridLayout, QVBoxLayout from game import Game from gen.ato import Package from gen.flights.flight import Flight +from qt_ui.models import PackageModel from qt_ui.windows.mission.flight.settings.QFlightDepartureDisplay import \ QFlightDepartureDisplay from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import \ @@ -19,14 +20,14 @@ from qt_ui.windows.mission.flight.settings.QCustomName import \ class QGeneralFlightSettingsTab(QFrame): on_flight_settings_changed = Signal() - def __init__(self, game: Game, package: Package, flight: Flight): + def __init__(self, game: Game, package_model: PackageModel, flight: Flight): super().__init__() layout = QGridLayout() flight_info = QFlightTypeTaskInfo(flight) - flight_departure = QFlightDepartureDisplay(package, flight) - flight_slots = QFlightSlotEditor(flight, game) - flight_start_type = QFlightStartType(flight) + flight_departure = QFlightDepartureDisplay(package_model, flight) + flight_slots = QFlightSlotEditor(package_model, flight, game) + flight_start_type = QFlightStartType(package_model, flight) flight_custom_name = QFlightCustomName(flight) layout.addWidget(flight_info, 0, 0) layout.addWidget(flight_departure, 1, 0) diff --git a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py index 92145a1c..abd1733d 100644 --- a/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py +++ b/qt_ui/windows/mission/flight/waypoints/QFlightWaypointTab.py @@ -29,8 +29,6 @@ from qt_ui.windows.mission.flight.waypoints \ class QFlightWaypointTab(QFrame): - on_flight_changed = Signal() - def __init__(self, game: Game, package: Package, flight: Flight): super(QFlightWaypointTab, self).__init__() self.game = game @@ -163,4 +161,3 @@ class QFlightWaypointTab(QFrame): def on_change(self): self.flight_waypoint_list.update_list() - self.on_flight_changed.emit()