#include "MainWindow.h" #include #include #include #include #include #include #include #include #include #include #include "BlueprintPanel.h" #include "BuildButtonGrid.h" #include "BuildingBlocksChangedEvent.h" #include "BuildingSystem.h" #include "ConfigLoader.h" #include "GameWorldView.h" #include "SchematicChoiceDialog.h" #include "HeaderBar.h" #include "SelectedBuildingPanel.h" #include "ShipLayoutBlueprintSerializer.h" #include "ShipLayoutDialog.h" #include "Simulation.h" #include "Tick.h" #include "VisualsLoader.h" MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* parent) : QWidget(parent) , m_configDir(configDir) , m_visuals(VisualsLoader::load(configDir + "/visuals.toml")) , m_sim(sim) { setWindowTitle(tr("Dota Factory")); resize(1280, 768); m_headerBar = new HeaderBar(this); m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, this); m_bottomPanel = new QWidget(this); QHBoxLayout* bottomLayout = new QHBoxLayout(m_bottomPanel); bottomLayout->setContentsMargins(0, 0, 0, 0); bottomLayout->setSpacing(0); m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_bottomPanel); m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_bottomPanel); m_blueprintPanel = new BlueprintPanel(sim, &sim->config(), m_bottomPanel); bottomLayout->addWidget(m_selectedBuildingPanel, 1); bottomLayout->addWidget(m_buildButtonGrid, 1); bottomLayout->addWidget(m_blueprintPanel, 1); // Signals: game world → other panels connect(m_gameWorldView, &GameWorldView::selectionChanged, m_selectedBuildingPanel, &SelectedBuildingPanel::onSelectionChanged); connect(m_gameWorldView, &GameWorldView::gameOver, this, &MainWindow::onGameOver); connect(m_gameWorldView, &GameWorldView::escapeMenuRequested, this, &MainWindow::onEscapeMenuRequested); connect(m_selectedBuildingPanel, &SelectedBuildingPanel::layoutDialogRequested, this, &MainWindow::onLayoutDialogRequested); // Signals: build grid → game world connect(m_buildButtonGrid, &BuildButtonGrid::buildingTypeSelected, m_gameWorldView, &GameWorldView::enterBuilderMode); connect(m_buildButtonGrid, &BuildButtonGrid::builderModeExited, m_gameWorldView, &GameWorldView::exitBuilderMode); connect(m_gameWorldView, &GameWorldView::builderModeExited, m_buildButtonGrid, &BuildButtonGrid::clearActiveButton); connect(m_buildButtonGrid, &BuildButtonGrid::demolishModeToggleRequested, m_gameWorldView, &GameWorldView::toggleDemolishMode); connect(m_gameWorldView, &GameWorldView::demolishModeChanged, m_buildButtonGrid, &BuildButtonGrid::setDemolishModeActive); // Signals: blueprint panel ↔ game world connect(m_gameWorldView, &GameWorldView::selectionChanged, m_blueprintPanel, &BlueprintPanel::onSelectionChanged); connect(m_blueprintPanel, &BlueprintPanel::blueprintPlacementRequested, m_gameWorldView, &GameWorldView::enterBlueprintMode); connect(m_blueprintPanel, &BlueprintPanel::exitBlueprintModeRequested, m_gameWorldView, &GameWorldView::exitBlueprintMode); connect(m_gameWorldView, &GameWorldView::blueprintModeExited, m_blueprintPanel, &BlueprintPanel::clearActiveBlueprintButton); // Signals: header bar → game world connect(m_headerBar, &HeaderBar::speedChanged, m_gameWorldView, &GameWorldView::setGameSpeed); 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; } const int remaining = totalH - headerH; const int gameH = remaining * 70 / 100; const int panelH = remaining - gameH; m_headerBar->setGeometry(0, 0, totalW, headerH); m_gameWorldView->setGeometry(0, headerH, totalW, gameH); m_bottomPanel->setGeometry(0, headerH + gameH, totalW, panelH); } void MainWindow::handleEvent(std::shared_ptr event) { m_buildButtonGrid->updateAffordability(event->blocks); } void MainWindow::handleEvent(std::shared_ptr event) { const double prevSpeed = m_gameWorldView->gameSpeed(); m_gameWorldView->setGameSpeed(0.0); SchematicChoiceDialog dialog(event->choices, this); dialog.exec(); m_sim->applySchematicChoice(dialog.getChosenIndex()); m_gameWorldView->setGameSpeed(prevSpeed); } void MainWindow::onEscapeMenuRequested() { const double prevSpeed = m_gameWorldView->gameSpeed(); m_gameWorldView->setGameSpeed(0.0); 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) { try { GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir); VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml"); m_visuals = std::move(newVisuals); m_sim->reset(std::move(newConfig)); } catch (const std::exception& e) { QMessageBox::critical(this, tr("Config Error"), tr("Failed to reload config:\n%1").arg(e.what())); m_gameWorldView->setGameSpeed(prevSpeed); return; } m_gameWorldView->resetForNewGame(); } else if (clicked == quitBtn) { close(); } else { m_gameWorldView->setGameSpeed(prevSpeed); } } void MainWindow::onLayoutDialogRequested(BuildingId shipyardId) { const double prevSpeed = m_gameWorldView->gameSpeed(); m_gameWorldView->setGameSpeed(0.0); const Building* b = m_sim->buildings().findBuilding(shipyardId); if (!b) { m_gameWorldView->setGameSpeed(prevSpeed); return; } ShipLayoutConfig currentLayout; if (b->shipLayout.has_value()) { currentLayout = *b->shipLayout; } std::set unlockedModuleIds; std::map moduleLevels; for (const ModuleDef& def : m_sim->config().modules.modules) { if (m_sim->isModuleSchematicUnlocked(def.id)) { unlockedModuleIds.insert(def.id); } moduleLevels[def.id] = m_sim->moduleSchematicLevel(def.id); } ShipLayoutDialog dialog(&m_sim->config(), b->recipeId, currentLayout, m_layoutBlueprints, std::move(unlockedModuleIds), std::move(moduleLevels), this); if (dialog.exec() == QDialog::Accepted && dialog.result().has_value()) { m_sim->buildings().setShipLayout(shipyardId, *dialog.result()); } m_gameWorldView->setGameSpeed(prevSpeed); } void MainWindow::onGameOver() { const Tick tick = m_sim->currentTick(); const int totalSeconds = static_cast(ticksToSeconds(tick)); const int minutes = totalSeconds / 60; const int seconds = totalSeconds % 60; 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) { try { GameConfig newConfig = ConfigLoader::loadFromDirectory(m_configDir); VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml"); m_visuals = std::move(newVisuals); m_sim->reset(std::move(newConfig)); } catch (const std::exception& e) { QMessageBox::critical(this, tr("Config Error"), tr("Failed to reload config:\n%1").arg(e.what())); return; } m_gameWorldView->resetForNewGame(); } else { close(); } }