576 lines
23 KiB
C++
576 lines
23 KiB
C++
#include "MainWindow.h"
|
|
#include "FactoryQueries.h"
|
|
|
|
#include <map>
|
|
#include <random>
|
|
#include <set>
|
|
|
|
#include <QApplication>
|
|
#include <QCloseEvent>
|
|
#include <QDialog>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QInputDialog>
|
|
#include <QLineEdit>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QResizeEvent>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "BlueprintLibrary.h"
|
|
#include "BlueprintSelectionDialog.h"
|
|
#include "BuildButtonBar.h"
|
|
#include "BuildingSystem.h"
|
|
#include "Command.h"
|
|
#include "CommandRequestedEvent.h"
|
|
#include "ConfigLoader.h"
|
|
#include "EventManager.h"
|
|
#include "GameWorldView.h"
|
|
#include "RecipeSelectionDialog.h"
|
|
#include "SchematicChoiceDialog.h"
|
|
#include "HeaderBar.h"
|
|
#include "SelectionPanel.h"
|
|
#include "ControlsPanel.h"
|
|
#include "ShipLayoutBlueprintSerializer.h"
|
|
#include "ShipLayoutDialog.h"
|
|
#include "BuildingIconCache.h"
|
|
#include "ItemIconCache.h"
|
|
#include "ModalPauseScope.h"
|
|
#include "Simulation.h"
|
|
#include "Tick.h"
|
|
#include "VisualsLoader.h"
|
|
|
|
MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|
std::shared_ptr<ParsedReplay> replay, QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_configDir(configDir)
|
|
, m_visuals(VisualsLoader::load(configDir + "/visuals.toml"))
|
|
, m_sim(sim)
|
|
, m_replay(std::move(replay))
|
|
{
|
|
setWindowTitle(tr("Dota Factory"));
|
|
resize(1280, 768);
|
|
|
|
// Item and building icons live alongside the config (siblings of the config dir),
|
|
// read from disk at runtime the same way visuals.toml is (REQ-UI-ITEM-ICON,
|
|
// REQ-UI-BUILD-ICON).
|
|
const QString configDirPath = QString::fromStdString(m_configDir);
|
|
m_itemIcons = std::make_unique<ItemIconCache>(
|
|
QDir::cleanPath(configDirPath + "/../icons/items"), &m_visuals);
|
|
m_buildingIcons = std::make_unique<BuildingIconCache>(
|
|
QDir::cleanPath(configDirPath + "/../icons/buildings"));
|
|
|
|
m_headerBar = new HeaderBar(sim, &sim->getConfig(), m_itemIcons.get(), this);
|
|
|
|
m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
|
|
m_itemIcons.get(), m_replay.get(), this);
|
|
|
|
// Floats over the game world at its bottom center, sized to its buttons
|
|
// (REQ-UI-BUILD-BAR). Creation order is the stacking order for siblings, so
|
|
// building it after the world view puts it above the world and its vignettes,
|
|
// and before the dim overlay keeps modals dimming it too (REQ-UI-MODAL-DIM).
|
|
// Its geometry comes from layoutPanels().
|
|
m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(),
|
|
m_buildingIcons.get(), m_itemIcons.get(),
|
|
this);
|
|
|
|
// The blueprints have no widget of their own: they are saved with Ctrl+C and picked
|
|
// from a modal dialog (REQ-UI-BLUEPRINT-DIALOG), both driven from this window
|
|
// because only it can pause the game and raise the dim overlay. Built after the
|
|
// world view because loading blueprints.toml may put a message box on screen.
|
|
m_blueprintLibrary = std::make_unique<BlueprintLibrary>(sim, &sim->getConfig(), this);
|
|
// Two facts about blueprints decide what the world view offers the player
|
|
// (REQ-UI-CONTROLS-CONTENT); the library is built after the view, so it is handed
|
|
// over here rather than passed to the constructor.
|
|
m_gameWorldView->setBlueprintLibrary(m_blueprintLibrary.get());
|
|
|
|
// Floats over the game world at its right edge rather than occupying a column of
|
|
// its own, and hides itself while nothing is selected (REQ-UI-SELECTION-PANEL). Like
|
|
// the build button bar it is a sibling of the world view built after it, which is
|
|
// what puts it above the world and its vignettes and below the dim overlay. It
|
|
// brings its own chrome; its geometry comes from layoutPanels().
|
|
m_selectionPanel = new SelectionPanel(sim, &sim->getConfig(), &m_visuals,
|
|
m_itemIcons.get(), m_buildingIcons.get(),
|
|
this);
|
|
|
|
// Floats at the world view's opposite edge from the selection panel and reads the
|
|
// world view for the player's current situation (REQ-UI-CONTROLS-PANEL). Built
|
|
// after the view for the same stacking reason as the panels above it.
|
|
m_controlsPanel = new ControlsPanel(m_gameWorldView, this);
|
|
|
|
// Created last so it stacks above the other children; covers the whole window and
|
|
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
|
|
m_dimOverlay = new ModalDimOverlay(m_visuals.overlays.modalDim, this);
|
|
|
|
m_gameWorldView->setFocus();
|
|
|
|
connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) {
|
|
if (newWidget && newWidget != m_gameWorldView && !QApplication::activeModalWidget())
|
|
{
|
|
m_gameWorldView->setFocus();
|
|
}
|
|
});
|
|
|
|
// Load layout blueprints from disk. Missing file is silently ignored.
|
|
const QString bpPath = QCoreApplication::applicationDirPath() + "/ship_layouts.toml";
|
|
QFile bpFile(bpPath);
|
|
if (bpFile.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
{
|
|
try
|
|
{
|
|
m_layoutBlueprints = ShipLayoutBlueprintSerializer::deserialize(
|
|
bpFile.readAll().toStdString());
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
QMessageBox::critical(this, tr("Load Error"),
|
|
tr("Failed to load ship_layouts.toml:\n%1").arg(e.what()));
|
|
m_layoutBlueprints.clear();
|
|
}
|
|
}
|
|
|
|
registerForEvents();
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
unregisterForEvents();
|
|
}
|
|
|
|
void MainWindow::resizeEvent(QResizeEvent* event)
|
|
{
|
|
QWidget::resizeEvent(event);
|
|
layoutPanels();
|
|
}
|
|
|
|
void MainWindow::closeEvent(QCloseEvent* event)
|
|
{
|
|
const QString path = QCoreApplication::applicationDirPath() + "/ship_layouts.toml";
|
|
QFile file(path);
|
|
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
|
|
{
|
|
try
|
|
{
|
|
const std::string content =
|
|
ShipLayoutBlueprintSerializer::serialize(m_layoutBlueprints);
|
|
file.write(QByteArray::fromStdString(content));
|
|
}
|
|
catch (...) {}
|
|
}
|
|
QWidget::closeEvent(event);
|
|
}
|
|
|
|
void MainWindow::layoutPanels()
|
|
{
|
|
const int totalW = width();
|
|
const int totalH = height();
|
|
const int headerH = m_headerBar->sizeHint().height();
|
|
if (headerH <= 0) { return; }
|
|
|
|
// Header bar and game world view span the full window width; the two floating
|
|
// widgets below are the only things over the world (REQ-UI-HEADER,
|
|
// REQ-UI-WORLD-SIZE).
|
|
const QRect worldRect(0, headerH, totalW, totalH - headerH);
|
|
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
|
m_gameWorldView->setGeometry(worldRect);
|
|
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
|
|
|
// The floating widgets are placed in one ordered pass, each into the space the
|
|
// earlier ones have not taken (FloatingPanel.h). The order is the priority the
|
|
// requirements state: the build button bar takes what it wants and never moves for
|
|
// anyone (REQ-UI-BUILD-BAR), the controls panel steps around the bar
|
|
// (REQ-UI-CONTROLS-PANEL), and the selection panel keeps clear of both
|
|
// (REQ-UI-SELECTION-PANEL). A widget with nothing to show hides itself in placeIn()
|
|
// and takes no space.
|
|
//
|
|
// Re-entry is refused rather than queued: setGeometry() on a widget in the pass can
|
|
// reach code that asks for another pass, and the one already running is about to
|
|
// produce the same answer.
|
|
if (m_layingOut)
|
|
{
|
|
return;
|
|
}
|
|
m_layingOut = true;
|
|
|
|
const std::vector<QWidget*> floatingWidgets = {
|
|
m_buildButtonBar, m_controlsPanel, m_selectionPanel };
|
|
const std::vector<FloatingPanel*> floatingPanels = {
|
|
m_buildButtonBar, m_controlsPanel, m_selectionPanel };
|
|
|
|
std::vector<QRect> occupiedRects;
|
|
for (std::size_t i = 0; i < floatingPanels.size(); ++i)
|
|
{
|
|
floatingPanels[i]->placeIn(worldRect, occupiedRects);
|
|
if (floatingWidgets[i]->isVisible())
|
|
{
|
|
occupiedRects.push_back(floatingWidgets[i]->geometry());
|
|
}
|
|
}
|
|
|
|
m_layingOut = false;
|
|
}
|
|
|
|
void MainWindow::handleEvent(
|
|
std::shared_ptr<const FloatingLayoutInvalidatedEvent> /*event*/)
|
|
{
|
|
// One of the floating widgets changed size or visibility. What each of them may take
|
|
// depends on the ones placed before it, so the answer is the whole pass rather than
|
|
// that one widget re-placing itself.
|
|
layoutPanels();
|
|
}
|
|
|
|
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
|
|
{
|
|
ModalPauseScope pause(*m_gameWorldView);
|
|
|
|
ModalDimScope dim(*m_dimOverlay);
|
|
SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes,
|
|
m_itemIcons.get(), m_buildingIcons.get(), this);
|
|
dialog.exec();
|
|
|
|
std::shared_ptr<ApplySchematicChoiceCommand> command =
|
|
std::make_shared<ApplySchematicChoiceCommand>();
|
|
command->choiceIndex = dialog.getChosenIndex();
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
}
|
|
|
|
void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*event*/)
|
|
{
|
|
ModalPauseScope pause(*m_gameWorldView);
|
|
|
|
ModalDimScope dim(*m_dimOverlay);
|
|
QMessageBox box(this);
|
|
box.setWindowTitle(tr("Paused"));
|
|
QPushButton* continueBtn = box.addButton(tr("Continue"), QMessageBox::AcceptRole);
|
|
QPushButton* restartBtn = box.addButton(tr("Restart"), QMessageBox::ResetRole);
|
|
QPushButton* quitBtn = box.addButton(tr("Quit"), QMessageBox::DestructiveRole);
|
|
box.setEscapeButton(continueBtn);
|
|
box.exec();
|
|
|
|
QAbstractButton* clicked = box.clickedButton();
|
|
if (clicked == restartBtn)
|
|
{
|
|
std::optional<GameConfig> newConfig = reloadConfig();
|
|
if (!newConfig.has_value())
|
|
{
|
|
return;
|
|
}
|
|
// Restart is a command boundary; the view resets when the drain applies
|
|
// it (see GameWorldView::onFrame). A fresh random seed starts a new run.
|
|
// resetForNewGame() sets the speed for the new run, so the pre-restart
|
|
// speed is deliberately not restored here.
|
|
pause.release();
|
|
std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
|
|
command->config = std::make_shared<GameConfig>(std::move(*newConfig));
|
|
command->seed = std::random_device{}();
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
}
|
|
else if (clicked == quitBtn)
|
|
{
|
|
pause.release();
|
|
close();
|
|
}
|
|
}
|
|
|
|
std::optional<GameConfig> MainWindow::reloadConfig()
|
|
{
|
|
// Config is reloaded from disk on every restart (REQ-CFG-RELOAD); a malformed
|
|
// file must not leave the window half-updated, so the visuals are only applied
|
|
// once both files have parsed.
|
|
try
|
|
{
|
|
GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir);
|
|
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
|
m_visuals = std::move(newVisuals);
|
|
m_dimOverlay->setDimColor(m_visuals.overlays.modalDim);
|
|
// The composed item squares carry the colors they were painted with, so they
|
|
// are dropped for the new ones to take effect (REQ-UI-ITEM-ICON).
|
|
m_itemIcons->clearPixmapCache();
|
|
return newConfig;
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
QMessageBox::critical(this, tr("Config Error"),
|
|
tr("Failed to reload config:\n%1").arg(e.what()));
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
|
|
const std::string& schematicId,
|
|
const ShipLayoutConfig& currentLayout)
|
|
{
|
|
ModalPauseScope pause(*m_gameWorldView);
|
|
|
|
std::set<std::string> unlockedModuleIds;
|
|
for (const ModuleDef& def : m_sim->getConfig().modules.modules)
|
|
{
|
|
if (m_sim->isModuleSchematicUnlocked(def.id))
|
|
{
|
|
unlockedModuleIds.insert(def.id);
|
|
}
|
|
}
|
|
|
|
ModalDimScope dim(*m_dimOverlay);
|
|
ShipLayoutDialog dialog(&m_sim->getConfig(), schematicId, currentLayout,
|
|
m_layoutBlueprints,
|
|
std::move(unlockedModuleIds),
|
|
m_gameWorldView->isDebugDrawEnabled(),
|
|
m_itemIcons.get(), this);
|
|
// Opened from the panel's "Configure" button (REQ-MOD-UI-PREVIEW) or straight after
|
|
// a schematic change (REQ-MOD-UI-AUTO-DIALOG), so it opens on the panel either way.
|
|
placeOnSelectionPanel(dialog);
|
|
if (dialog.exec() == QDialog::Accepted && dialog.getResult().has_value())
|
|
{
|
|
std::shared_ptr<SetShipLayoutCommand> command =
|
|
std::make_shared<SetShipLayoutCommand>();
|
|
command->id = shipyardId;
|
|
command->layout = *dialog.getResult();
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
}
|
|
}
|
|
|
|
void MainWindow::placeOnSelectionPanel(QDialog& dialog) const
|
|
{
|
|
// The panel is up whenever one of these modals opens -- they are opened from its own
|
|
// controls, and it is shown whenever anything is selected (REQ-UI-EMPTY-SELECTION).
|
|
// Were it not, there would be no rectangle to center on and Qt's own centering on
|
|
// this window stands.
|
|
if (!m_selectionPanel->isVisible()) { return; }
|
|
|
|
// The dialog has never been shown, so it is still at its default size until its
|
|
// layout has run; centering it before that would use the wrong extent.
|
|
dialog.adjustSize();
|
|
const QSize dialogSize = dialog.size();
|
|
|
|
// The panel's live geometry, so a panel the player has dragged
|
|
// (REQ-UI-SELECTION-PANEL-DRAG) carries the modal with it.
|
|
const QRect panelRect(m_selectionPanel->mapToGlobal(QPoint(0, 0)),
|
|
m_selectionPanel->size());
|
|
const QRect windowRect(mapToGlobal(QPoint(0, 0)), size());
|
|
|
|
QPoint topLeft(panelRect.center().x() - dialogSize.width() / 2,
|
|
panelRect.center().y() - dialogSize.height() / 2);
|
|
|
|
// Pushed back inside the window, never resized to fit (REQ-UI-PANEL-MODAL). The far
|
|
// edge is clamped first and the near edge second, which is what aligns a dialog too
|
|
// large for the window with the window's top-left corner rather than pushing it off
|
|
// the opposite edge.
|
|
topLeft.setX(qMax(windowRect.left(),
|
|
qMin(topLeft.x(), windowRect.right() - dialogSize.width() + 1)));
|
|
topLeft.setY(qMax(windowRect.top(),
|
|
qMin(topLeft.y(), windowRect.bottom() - dialogSize.height() + 1)));
|
|
|
|
// Positions the dialog's frame, whose size is not known until it is first shown, so
|
|
// the result sits low by the title bar height against a true center -- measuring it
|
|
// would mean showing the dialog at the wrong place first. The move also marks the
|
|
// dialog as positioned, which is what stops QDialog from centering it on this window
|
|
// when it is shown.
|
|
dialog.move(topLeft);
|
|
}
|
|
|
|
void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event)
|
|
{
|
|
// A construction site has no Building yet; fall back to its site record so
|
|
// the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG).
|
|
const Building* b = findBuilding(m_sim->getFactoryState(), event->shipyardId);
|
|
const ConstructionSite* s =
|
|
b ? nullptr : findSite(m_sim->getFactoryState(), event->shipyardId);
|
|
if (!b && !s)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const std::string& schematicId = b ? b->recipeId : s->recipeId;
|
|
const std::optional<ShipLayoutConfig>& layoutOpt =
|
|
b ? b->shipLayout : s->shipLayout;
|
|
|
|
ShipLayoutConfig currentLayout;
|
|
if (layoutOpt.has_value())
|
|
{
|
|
currentLayout = *layoutOpt;
|
|
}
|
|
|
|
openShipLayoutDialog(event->shipyardId, schematicId, currentLayout);
|
|
}
|
|
|
|
void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event)
|
|
{
|
|
ModalPauseScope pause(*m_gameWorldView);
|
|
|
|
// A construction site has no Building yet; fall back to its site record so
|
|
// the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG).
|
|
const Building* b = findBuilding(m_sim->getFactoryState(), event->buildingId);
|
|
const ConstructionSite* s =
|
|
b ? nullptr : findSite(m_sim->getFactoryState(), event->buildingId);
|
|
if (!b && !s)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Held across both the selection dialog and any auto-opened layout dialog so the
|
|
// dim stays continuously visible through that sequence (REQ-UI-MODAL-DIM).
|
|
ModalDimScope dim(*m_dimOverlay);
|
|
|
|
const BuildingType type = b ? b->type : s->type;
|
|
// Captured as a copy: a queued command may drain during the modal dialog's
|
|
// event loop and reallocate the building vectors, so b/s must not be
|
|
// dereferenced after dialog.exec() returns.
|
|
const std::string oldSchematic = b ? b->recipeId : s->recipeId;
|
|
const std::vector<RecipeSelectionOption> options =
|
|
buildRecipeSelectionOptions(type, *m_sim, m_sim->getConfig());
|
|
const QString title = (type == BuildingType::Shipyard)
|
|
? tr("Select Schematic")
|
|
: tr("Select Recipe");
|
|
|
|
bool autoOpenLayout = false;
|
|
std::string chosenSchematic;
|
|
RecipeSelectionDialog dialog(options, title, m_itemIcons.get(),
|
|
m_buildingIcons.get(), this);
|
|
placeOnSelectionPanel(dialog);
|
|
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
|
|
{
|
|
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
|
|
command->id = event->buildingId;
|
|
command->recipeId = *dialog.getChosenId();
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
|
|
// REQ-MOD-UI-AUTO-DIALOG: picking a new schematic for a shipyard opens the
|
|
// layout configuration dialog immediately. Only on an actual change.
|
|
if (type == BuildingType::Shipyard && *dialog.getChosenId() != oldSchematic)
|
|
{
|
|
autoOpenLayout = true;
|
|
chosenSchematic = *dialog.getChosenId();
|
|
}
|
|
}
|
|
|
|
// The SetRecipeCommand above is queued (drains on a later frame) and clears
|
|
// the shipyard's layout, so open the dialog with the chosen schematic and an
|
|
// empty layout rather than reading the not-yet-updated building state. Speed
|
|
// is restored first so the helper snapshots the real speed to restore.
|
|
pause.restore();
|
|
if (autoOpenLayout)
|
|
{
|
|
openShipLayoutDialog(event->buildingId, chosenSchematic, ShipLayoutConfig{});
|
|
}
|
|
}
|
|
|
|
void MainWindow::handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> /*event*/)
|
|
{
|
|
// Ctrl+C has effect only when something player-placeable is selected; otherwise no
|
|
// dialog opens at all (REQ-UI-BLUEPRINT-CREATE).
|
|
if (!m_blueprintLibrary->getCanCaptureSelection()) { return; }
|
|
|
|
// Both scopes are held across the naming dialog and the selection dialog it hands
|
|
// over to, so the dim never blinks off and the simulation is not resumed in between
|
|
// (REQ-UI-MODAL-DIM).
|
|
ModalPauseScope pause(*m_gameWorldView);
|
|
ModalDimScope dim(*m_dimOverlay);
|
|
|
|
bool ok = false;
|
|
const QString name = QInputDialog::getText(
|
|
this, tr("Create Blueprint"), tr("Blueprint name:"), QLineEdit::Normal,
|
|
QString(), &ok);
|
|
// Cancel, Escape, or an empty name: no blueprint, and no selection dialog.
|
|
if (!ok || name.trimmed().isEmpty()) { return; }
|
|
|
|
m_blueprintLibrary->saveSelectionAs(name.trimmed());
|
|
showBlueprintSelectionDialog();
|
|
}
|
|
|
|
void MainWindow::handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> /*event*/)
|
|
{
|
|
ModalPauseScope pause(*m_gameWorldView);
|
|
ModalDimScope dim(*m_dimOverlay);
|
|
showBlueprintSelectionDialog();
|
|
}
|
|
|
|
void MainWindow::showBlueprintSelectionDialog()
|
|
{
|
|
BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), m_itemIcons.get(), this);
|
|
if (dialog.exec() == QDialog::Accepted && dialog.getChosenIndex().has_value())
|
|
{
|
|
// Entered after the dialog has closed, which is the order REQ-UI-BLUEPRINT-CARD
|
|
// describes: clicking a card closes the dialog and enters placement mode.
|
|
m_blueprintLibrary->beginPlacement(*dialog.getChosenIndex());
|
|
}
|
|
}
|
|
|
|
void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
|
{
|
|
const Tick tick = m_sim->getCurrentTick();
|
|
const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
|
|
const int minutes = totalSeconds / 60;
|
|
const int seconds = totalSeconds % 60;
|
|
|
|
ModalDimScope dim(*m_dimOverlay);
|
|
QMessageBox box(this);
|
|
box.setWindowTitle(tr("Game Over"));
|
|
box.setText(tr("HQ destroyed!\nSurvival time: %1:%2")
|
|
.arg(minutes, 2, 10, QChar('0'))
|
|
.arg(seconds, 2, 10, QChar('0')));
|
|
QPushButton* restartBtn = box.addButton(tr("Restart"), QMessageBox::AcceptRole);
|
|
box.addButton(tr("Quit"), QMessageBox::RejectRole);
|
|
box.exec();
|
|
|
|
if (box.clickedButton() == restartBtn)
|
|
{
|
|
std::optional<GameConfig> newConfig = reloadConfig();
|
|
if (!newConfig.has_value())
|
|
{
|
|
return;
|
|
}
|
|
// Restart is a command boundary; the view resets when the drain applies it.
|
|
std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
|
|
command->config = std::make_shared<GameConfig>(std::move(*newConfig));
|
|
command->seed = std::random_device{}();
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
}
|
|
else
|
|
{
|
|
close();
|
|
}
|
|
}
|
|
|
|
void MainWindow::handleEvent(std::shared_ptr<const WinEvent> /*event*/)
|
|
{
|
|
const Tick tick = m_sim->getCurrentTick();
|
|
const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
|
|
const int minutes = totalSeconds / 60;
|
|
const int seconds = totalSeconds % 60;
|
|
|
|
ModalDimScope dim(*m_dimOverlay);
|
|
QMessageBox box(this);
|
|
box.setWindowTitle(tr("Won!"));
|
|
box.setText(tr("You collected all artifacts!\nSurvival time: %1:%2")
|
|
.arg(minutes, 2, 10, QChar('0'))
|
|
.arg(seconds, 2, 10, QChar('0')));
|
|
QPushButton* restartBtn = box.addButton(tr("Restart"), QMessageBox::AcceptRole);
|
|
box.addButton(tr("Quit"), QMessageBox::RejectRole);
|
|
box.exec();
|
|
|
|
if (box.clickedButton() == restartBtn)
|
|
{
|
|
std::optional<GameConfig> newConfig = reloadConfig();
|
|
if (!newConfig.has_value())
|
|
{
|
|
return;
|
|
}
|
|
// Restart is a command boundary; the view resets when the drain applies it.
|
|
std::shared_ptr<ResetCommand> command = std::make_shared<ResetCommand>();
|
|
command->config = std::make_shared<GameConfig>(std::move(*newConfig));
|
|
command->seed = std::random_device{}();
|
|
EventManager::getInstance()->sendEventImmediately(
|
|
std::make_shared<CommandRequestedEvent>(command));
|
|
}
|
|
else
|
|
{
|
|
close();
|
|
}
|
|
}
|