315 lines
10 KiB
C++
315 lines
10 KiB
C++
#include "MainWindow.h"
|
|
|
|
#include <map>
|
|
#include <set>
|
|
|
|
#include <QApplication>
|
|
#include <QCloseEvent>
|
|
#include <QFile>
|
|
#include <QHBoxLayout>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QResizeEvent>
|
|
#include <QVBoxLayout>
|
|
|
|
#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<const BuildingBlocksChangedEvent> event)
|
|
{
|
|
m_buildButtonGrid->updateAffordability(event->blocks);
|
|
}
|
|
|
|
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> 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<std::string> unlockedModuleIds;
|
|
std::map<std::string, int> 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<int>(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();
|
|
}
|
|
}
|