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