Implements the requirements of 0bf24a7. Recipes were explained in prose:
the recipe dialog was a grid of icon-only buttons with a text tooltip
listing name, inputs, time and output, the unlock-choice dialog listed
recipe names with the same tooltip, an item chip said nothing about where
its item came from, and a module button never showed its cost.
RecipeLineRow is the one widget that draws a recipe -- optional building
chip and name, inputs, arrow, outputs, time -- and every place that states
what something makes now goes through it: the panel's recipe summary (which
it replaces RecipeSummaryRow for), the selection dialog's option buttons,
the item production tooltip, the unlock-choice dialog's recipe lines, the
module buttons' cost lines, and the layout dialog's build cost. Its arrow
is drawn only where there are outputs, so a shipyard summary no longer
points at nothing.
ItemProducers answers where an item comes from, filtering by unlock state
per building type, and ItemTooltip draws it as a window of its own -- Qt's
tooltips are text, this one has item squares and building chips in it.
ItemChip pops it after a hover delay.
The selection dialog becomes one scrolling vertical column of buttons, each
showing its name over its recipe line, and no dialog carries a tooltip any
more. RecipeTooltip and recipes.toml's unused "icon" field go with the
behavior they served.
Two requirement amendments the implementation forced, both in this commit:
the option column scrolls when taller than the window allows, and a Smelter
or Reprocessing recipe is listed once its building is unlocked, those
recipes carrying no unlock of their own. The layout dialog's build cost
also moved out of the shared ShipStatsPanel, which the balancing tool
compiles directly and is deliberately kept free of the icon caches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
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();
|
|
}
|
|
}
|