Files
dota_factory/src/ui/MainWindow.cpp

288 lines
9.5 KiB
C++

#include "MainWindow.h"
#include <QApplication>
#include <QCloseEvent>
#include <QFile>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QPushButton>
#include <QResizeEvent>
#include <QVBoxLayout>
#include "BlueprintPanel.h"
#include "BuildButtonGrid.h"
#include "BuildingSystem.h"
#include "ConfigLoader.h"
#include "GameWorldView.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("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::stateUpdated,
m_headerBar, &HeaderBar::onStateUpdated);
connect(m_gameWorldView, &GameWorldView::stateUpdated,
this, &MainWindow::onStateUpdated); // for affordability
connect(m_gameWorldView, &GameWorldView::stateUpdated,
m_selectedBuildingPanel, &SelectedBuildingPanel::onStateUpdated);
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_gameWorldView, &GameWorldView::stateUpdated,
m_blueprintPanel, &BlueprintPanel::onStateUpdated);
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, "Load Error",
QString("Failed to load ship_layouts.toml:\n%1").arg(e.what()));
m_layoutBlueprints.clear();
}
}
}
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::onStateUpdated(Tick /*tick*/, int blocks, double /*speed*/)
{
m_buildButtonGrid->updateAffordability(blocks);
}
void MainWindow::onEscapeMenuRequested()
{
const double prevSpeed = m_gameWorldView->gameSpeed();
m_gameWorldView->setGameSpeed(0.0);
QMessageBox box(this);
box.setWindowTitle("Paused");
QPushButton* continueBtn = box.addButton("Continue", QMessageBox::AcceptRole);
QPushButton* restartBtn = box.addButton("Restart", QMessageBox::ResetRole);
QPushButton* quitBtn = box.addButton("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, "Config Error",
QString("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;
}
ShipLayoutDialog dialog(&m_sim->config(), b->recipeId, currentLayout,
m_layoutBlueprints, 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<int>(ticksToSeconds(tick));
const int minutes = totalSeconds / 60;
const int seconds = totalSeconds % 60;
QMessageBox box(this);
box.setWindowTitle("Game Over");
box.setText(QString("HQ destroyed!\nSurvival time: %1:%2")
.arg(minutes, 2, 10, QChar('0'))
.arg(seconds, 2, 10, QChar('0')));
QPushButton* restartBtn = box.addButton("Restart", QMessageBox::AcceptRole);
box.addButton("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, "Config Error",
QString("Failed to reload config:\n%1").arg(e.what()));
return;
}
m_gameWorldView->resetForNewGame();
}
else
{
close();
}
}