diff --git a/changelog.md b/changelog.md index 3d31e041..ebf3e594 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ Saves from 7.x are not compatible with 8.0. * **[Engine]** Support for DCS 2.8.6.41066, including the new Sinai map. * **[UI]** Limited size of overfull airbase display and added scrollbar. * **[UI]** Moved air wing and transfer menus to the toolbar to improve UI fit on low resolution displays. +* **[UI]** Added basic game over dialog. ## Fixes diff --git a/game/game.py b/game/game.py index 9c45763c..d086b508 100644 --- a/game/game.py +++ b/game/game.py @@ -5,7 +5,6 @@ import logging import math from collections.abc import Iterator from datetime import date, datetime, time, timedelta -from enum import Enum from typing import Any, List, TYPE_CHECKING, Type, Union, cast from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers @@ -37,6 +36,7 @@ from .theater.theatergroundobject import ( ) from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder from .timeofday import TimeOfDay +from .turnstate import TurnState from .weather.conditions import Conditions if TYPE_CHECKING: @@ -81,12 +81,6 @@ AWACS_BUDGET_COST = 4 PLAYER_BUDGET_IMPORTANCE_LOG = 2 -class TurnState(Enum): - WIN = 0 - LOSS = 1 - CONTINUE = 2 - - class Game: def __init__( self, diff --git a/game/turnstate.py b/game/turnstate.py new file mode 100644 index 00000000..80811f6d --- /dev/null +++ b/game/turnstate.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from enum import Enum + + +class TurnState(Enum): + WIN = 0 + LOSS = 1 + CONTINUE = 2 diff --git a/qt_ui/cheatcontext.py b/qt_ui/cheatcontext.py new file mode 100644 index 00000000..d3fe6659 --- /dev/null +++ b/qt_ui/cheatcontext.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from collections.abc import Iterator +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from game.server import EventStream +from game.turnstate import TurnState +from qt_ui.windows.GameUpdateSignal import GameUpdateSignal +from qt_ui.windows.gameoverdialog import GameOverDialog + +if TYPE_CHECKING: + from game import Game + from game.sim import GameUpdateEvents + + +@contextmanager +def game_state_modifying_cheat_context(game: Game) -> Iterator[GameUpdateEvents]: + with EventStream.event_context() as events: + yield events + + state = game.check_win_loss() + if state is not TurnState.CONTINUE: + dialog = GameOverDialog(won=state is TurnState.WIN) + dialog.exec() + else: + game.initialize_turn(events) + GameUpdateSignal.get_instance().updateGame(game) diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 1c92a0d6..195a201b 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -24,6 +24,7 @@ from game.persistence import SaveManager from game.server import EventStream, GameContext from game.server.dependencies import QtCallbacks, QtContext from game.theater import ControlPoint, MissionTarget, TheaterGroundObject +from game.turnstate import TurnState from qt_ui import liberation_install from qt_ui.dialogs import Dialog from qt_ui.models import GameModel @@ -39,6 +40,7 @@ from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog from qt_ui.windows.QDebriefingWindow import QDebriefingWindow from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2 +from qt_ui.windows.gameoverdialog import GameOverDialog from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu from qt_ui.windows.infos.QInfoPanel import QInfoPanel from qt_ui.windows.logs.QLogsWindow import QLogsWindow @@ -559,8 +561,13 @@ class QLiberationWindow(QMainWindow): logging.info("On Debriefing") self.debriefing = QDebriefingWindow(debrief) self.debriefing.exec() - self.game.pass_turn() - GameUpdateSignal.get_instance().updateGame(self.game) + + state = self.game.check_win_loss() + if state is not TurnState.CONTINUE: + GameOverDialog(won=state is TurnState.WIN, parent=self).exec() + else: + self.game.pass_turn() + GameUpdateSignal.get_instance().updateGame(self.game) def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None: QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show() diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 4b2a1e6c..fbc93431 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -9,19 +9,17 @@ from PySide6.QtWidgets import ( QVBoxLayout, QWidget, ) -from dcs.ships import Stennis, KUZNECOW from game import Game from game.ato.flighttype import FlightType from game.config import RUNWAY_REPAIR_COST -from game.server import EventStream -from game.sim import GameUpdateEvents from game.theater import ( AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION, ControlPoint, ControlPointType, FREE_FRONTLINE_UNIT_SUPPLY, ) +from qt_ui.cheatcontext import game_state_modifying_cheat_context from qt_ui.dialogs import Dialog from qt_ui.models import GameModel from qt_ui.uiconstants import EVENT_ICONS @@ -119,13 +117,11 @@ class QBaseMenu2(QDialog): return self.game_model.game.settings.enable_base_capture_cheat def cheat_capture(self) -> None: - events = GameUpdateEvents() - self.cp.capture(self.game_model.game, events, for_player=not self.cp.captured) - # Reinitialized ground planners and the like. The ATO needs to be reset because - # missions planned against the flipped base are no longer valid. - self.game_model.game.initialize_turn(events) - EventStream.put_nowait(events) - GameUpdateSignal.get_instance().updateGame(self.game_model.game) + with game_state_modifying_cheat_context(self.game_model.game) as events: + self.cp.capture( + self.game_model.game, events, for_player=not self.cp.captured + ) + self.close() @property def has_transfer_destinations(self) -> bool: diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py index df59c091..ac37e7c6 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategy.py @@ -3,10 +3,8 @@ from collections.abc import Callable from PySide6.QtWidgets import QGroupBox, QLabel, QPushButton, QVBoxLayout from game import Game -from game.server import EventStream -from game.sim.gameupdateevents import GameUpdateEvents from game.theater import ControlPoint -from qt_ui.windows.GameUpdateSignal import GameUpdateSignal +from qt_ui.cheatcontext import game_state_modifying_cheat_context from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import ( QGroundForcesStrategySelector, ) @@ -52,15 +50,12 @@ class QGroundForcesStrategy(QGroupBox): self.setLayout(layout) def cheat_alter_front_line(self, enemy_point: ControlPoint, advance: bool) -> None: - amount = 0.2 - if not advance: - amount *= -1 - self.cp.base.affect_strength(amount) - enemy_point.base.affect_strength(-amount) - front_line = self.cp.front_line_with(enemy_point) - front_line.update_position() - events = GameUpdateEvents().update_front_line(front_line) - # Clear the ATO to replan missions affected by the front line. - self.game.initialize_turn(events) - EventStream.put_nowait(events) - GameUpdateSignal.get_instance().updateGame(self.game) + with game_state_modifying_cheat_context(self.game) as events: + amount = 0.2 + if not advance: + amount *= -1 + self.cp.base.affect_strength(amount) + enemy_point.base.affect_strength(-amount) + front_line = self.cp.front_line_with(enemy_point) + front_line.update_position() + events.update_front_line(front_line) diff --git a/qt_ui/windows/gameoverdialog.py b/qt_ui/windows/gameoverdialog.py new file mode 100644 index 00000000..78ae24bf --- /dev/null +++ b/qt_ui/windows/gameoverdialog.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from PySide6.QtWidgets import ( + QDialog, + QVBoxLayout, + QLabel, + QHBoxLayout, + QPushButton, + QWidget, +) + +from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard + + +class GameOverDialog(QDialog): + def __init__(self, won: bool, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setModal(True) + self.setWindowTitle("Game Over") + + layout = QVBoxLayout() + self.setLayout(layout) + + layout.addWidget( + QLabel( + f"You {'won' if won else 'lost'}!
" + "
" + "Click below to start a new game." + ) + ) + button_row = QHBoxLayout() + layout.addLayout(button_row) + + button_row.addStretch() + + new_game = QPushButton("New Game") + new_game.clicked.connect(self.on_new_game) + button_row.addWidget(new_game) + + def on_new_game(self) -> None: + wizard = NewGameWizard(self) + wizard.show() + wizard.accepted.connect(self.accept) diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 0c99d19c..e0e7c84f 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -95,6 +95,7 @@ def wrap_label_text(text: str, width: int = 100) -> str: class NewGameWizard(QtWidgets.QWizard): def __init__(self, parent=None): super(NewGameWizard, self).__init__(parent) + self.setModal(True) # The wizard should probably be refactored to edit this directly, but for now we # just create a Settings object so that we can load the player's preserved