Files
dota_factory/src/ui/MainWindow.cpp
Malte Langkabel abe8c8f448 stand the expansion button on the ground it buys
The button leaves the header bar for the world, centered on the columns
the next purchase unlocks and panning with them until it leaves the view.
Those columns now take a lighter tint than the locked ground behind them,
so the boundary between the two says how far one purchase reaches.

Nothing in the code reported a scroll: the camera knew it had moved and
told only the hover. A ViewScrolledEvent says it now, which is what a
widget keeping a place in the world rather than on the screen needs. The
panels want none of it -- they are placed against the screen and stay put
as the world moves under them -- so the button is placed after their pass
and never joins the rectangles they step around.

Its face is two lines, "Expand" over the cost. That makes the cost a
display of its own, so it carries the building-block tooltip the way every
other item value does, on hover alone since the click buys the expansion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
2026-08-18 22:55:06 +02:00

648 lines
26 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 <QMessageBox>
#include <QPoint>
#include <QResizeEvent>
#include <QVBoxLayout>
#include <QVector2D>
#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 "ExpandButton.h"
#include "ShipLayoutBlueprintSerializer.h"
#include "ShipLayoutDialog.h"
#include "BuildingIconCache.h"
#include "ItemIconCache.h"
#include "MessageDialog.h"
#include "ModalLayer.h"
#include "ModalPauseScope.h"
#include "NameInputDialog.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(getItemTooltipContext(), 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);
// Stands in the world rather than on the screen, on the columns the next expansion
// unlocks (REQ-UI-EXPAND-BUTTON). A sibling of the world view like the panels, which
// is what keeps the builder-mode ghost off the tile beneath it and the click off the
// world (REQ-BLD-GHOST): the view's hover follows underMouse(), false while the
// cursor rests on a sibling.
m_expandButton = new ExpandButton(getItemTooltipContext(), 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,
// dims the game behind every modal, and is what those modals are drawn on
// (REQ-UI-MODAL-DIM, REQ-UI-MODAL-CHROME).
m_modalLayer = new ModalLayer(m_visuals.overlays.modalDim, this);
m_gameWorldView->setFocus();
connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) {
// A modal holds the focus while it is open, whether it is one of ours on the
// layer or a system message box (REQ-UI-MODAL-CHROME). A null widget -- the focus
// going nowhere at all -- is caught too: the world view is what answers keys when
// no modal is up, so leaving the window with no focus widget would leave the
// hotkeys dead (REQ-UI-HOTKEYS).
if (newWidget != m_gameWorldView && !QApplication::activeModalWidget()
&& !m_modalLayer->isActive())
{
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)
{
// A modal on the layer runs a nested event loop over widgets this window owns, so
// the window must outlive it. A modal window used to block the close outright; this
// does the same for a modal that is only a widget (REQ-UI-MODAL-CHROME).
if (m_modalLayer->isActive())
{
event->ignore();
return;
}
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_modalLayer->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());
}
}
// Last, and never added to occupiedRects: it marks a place in the world, so the
// panels do not step around it and it does not step around them
// (REQ-UI-EXPAND-BUTTON).
placeExpandButton();
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);
SchematicChoiceDialog dialog(event->choices, m_sim->getConfig().recipes,
getItemTooltipContext(), m_modalLayer);
m_modalLayer->execute(dialog);
// The command goes out unconditionally because the dialog cannot be dismissed: it
// returns only once an option was clicked, so the index always names that option
// (REQ-DEF-SCHEMATIC-DROP). It is also the only thing that resolves the drop -- the
// poll that opened this dialog will not open it again while the choices stay pending
// (GameWorldView::onFrame) -- so a path that skipped the command would strand it.
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);
MessageDialog box(tr("Paused"), QString(), m_modalLayer);
const int continueIndex = box.addButton(tr("Continue"));
const int restartIndex = box.addButton(tr("Restart"));
const int quitIndex = box.addButton(tr("Quit"));
// Escape stands for Continue, as it did when this was a system box
// (REQ-UI-GAME-MENU).
box.setEscapeButtonIndex(continueIndex);
m_modalLayer->execute(box);
const std::optional<int> clicked = box.getClickedButtonIndex();
if (clicked == restartIndex)
{
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 == quitIndex)
{
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_modalLayer->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);
}
}
ShipLayoutDialog dialog(schematicId, currentLayout,
m_layoutBlueprints,
std::move(unlockedModuleIds),
m_gameWorldView->isDebugDrawEnabled(),
getItemTooltipContext(), m_modalLayer);
// 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.
if (m_modalLayer->execute(dialog, getSelectionPanelAnchor()) == 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));
}
}
QRect MainWindow::getSelectionPanelAnchor() 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 the layer's own centering
// stands (REQ-UI-PANEL-MODAL).
if (!m_selectionPanel->isVisible()) { return QRect(); }
// The panel's live geometry, so a panel the player has dragged
// (REQ-UI-SELECTION-PANEL-DRAG) carries the modal with it. Panel and layer are both
// children of this window, so the panel's geometry needs no mapping.
return m_selectionPanel->geometry();
}
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;
// Nothing to configure without a schematic and a grid to place modules on. The
// Configure button that publishes this is already disabled then (REQ-MOD-UI-PREVIEW),
// so this guards the event rather than the button: the dialog would otherwise open
// over a grid of no cells.
if (!m_sim->getConfig().ships.findLayoutShipDef(schematicId))
{
return;
}
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).
ModalLayerHold dim(*m_modalLayer);
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, getItemTooltipContext(),
m_modalLayer);
if (m_modalLayer->execute(dialog, getSelectionPanelAnchor()) == 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, and only
// for an actual schematic: "(None)" clears the shipyard and carries the empty
// id (REQ-UI-SELECT-OPTIONS), which differs from whatever was set but is not a
// schematic to configure.
if (type == BuildingType::Shipyard && *dialog.getChosenId() != oldSchematic
&& m_sim->getConfig().ships.findLayoutShipDef(*dialog.getChosenId()))
{
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);
ModalLayerHold dim(*m_modalLayer);
NameInputDialog nameDialog(tr("Create Blueprint"), tr("Blueprint name:"),
m_modalLayer);
const int result = m_modalLayer->execute(nameDialog);
// Cancel, Escape, or an empty name: no blueprint, and no selection dialog.
if (result != QDialog::Accepted || nameDialog.getName().isEmpty()) { return; }
m_blueprintLibrary->saveSelectionAs(nameDialog.getName());
showBlueprintSelectionDialog();
}
void MainWindow::handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> /*event*/)
{
ModalPauseScope pause(*m_gameWorldView);
showBlueprintSelectionDialog();
}
void MainWindow::placeExpandButton()
{
// Null while the button is still being built: refreshing its cost asks for a
// placement pass, and the first refresh happens inside its own constructor.
if (m_expandButton == nullptr)
{
return;
}
// The columns the next purchase unlocks: the ones immediately left of the buildable
// edge (REQ-GW-ASTEROID-EXPAND, REQ-UI-LOCKED-ASTEROID). Their middle, and the
// world's middle vertically, is where the button goes (REQ-UI-EXPAND-BUTTON).
const int asteroidWidth_tiles = m_sim->getCurrentAsteroidWidth_tiles();
const float columns_tiles = static_cast<float>(
m_sim->getConfig().world.expansion.columnsPerExpansion_tiles);
const QVector2D center_tiles(
-static_cast<float>(asteroidWidth_tiles) - columns_tiles / 2.0f,
static_cast<float>(m_sim->getConfig().world.heightTiles) / 2.0f);
// The transform is the view's, so the point it yields is in the view's coordinates;
// the button is a sibling of the view, so it is lifted into this window's.
const QPointF centerInView_px =
m_gameWorldView->getCoordinates().worldToWidget(center_tiles);
const QPoint center_px = m_gameWorldView->geometry().topLeft()
+ QPoint(static_cast<int>(centerInView_px.x()),
static_cast<int>(centerInView_px.y()));
// Neither clamped nor hidden: the button belongs to that ground and leaves the view
// with it, which the window's own edge takes care of (REQ-UI-EXPAND-BUTTON).
const QSize buttonSize = m_expandButton->sizeHint();
m_expandButton->setGeometry(QRect(
center_px - QPoint(buttonSize.width() / 2, buttonSize.height() / 2),
buttonSize));
}
void MainWindow::handleEvent(std::shared_ptr<const ViewScrolledEvent> /*event*/)
{
// The ground moved under the button; nothing else about the layout changed, so this
// is the one widget to re-place (REQ-UI-EXPAND-BUTTON).
placeExpandButton();
}
ItemTooltipContext MainWindow::getItemTooltipContext() const
{
return ItemTooltipContext{ m_sim, &m_sim->getConfig(), m_itemIcons.get(),
m_buildingIcons.get() };
}
void MainWindow::showBlueprintSelectionDialog()
{
BlueprintSelectionDialog dialog(m_blueprintLibrary.get(), getItemTooltipContext(),
m_modalLayer);
if (m_modalLayer->execute(dialog) == 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;
MessageDialog box(tr("Game Over"),
tr("HQ destroyed!\nSurvival time: %1:%2")
.arg(minutes, 2, 10, QChar('0'))
.arg(seconds, 2, 10, QChar('0')),
m_modalLayer);
const int restartIndex = box.addButton(tr("Restart"));
const int quitIndex = box.addButton(tr("Quit"));
// Escape quits, which is where the system box's reject role sent it.
box.setEscapeButtonIndex(quitIndex);
m_modalLayer->execute(box);
if (box.getClickedButtonIndex() == restartIndex)
{
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;
MessageDialog box(tr("Won!"),
tr("You collected all artifacts!\nSurvival time: %1:%2")
.arg(minutes, 2, 10, QChar('0'))
.arg(seconds, 2, 10, QChar('0')),
m_modalLayer);
const int restartIndex = box.addButton(tr("Restart"));
const int quitIndex = box.addButton(tr("Quit"));
// Escape quits, which is where the system box's reject role sent it.
box.setEscapeButtonIndex(quitIndex);
m_modalLayer->execute(box);
if (box.getClickedButtonIndex() == restartIndex)
{
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();
}
}