implement ui
This commit is contained in:
@@ -71,16 +71,44 @@ unset(SRCS)
|
|||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# ui — QtWidgets + QOpenGLWidget
|
# ui — QtWidgets + QOpenGLWidget
|
||||||
# Depends on lib. No sources yet; declared as INTERFACE library.
|
# Depends on lib.
|
||||||
# When UI source files are added under src/ui/, change this to
|
|
||||||
# a regular static library and enable AUTOMOC on the target.
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
set(HDRS)
|
||||||
|
set(SRCS)
|
||||||
|
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
|
|
||||||
add_library(${TARGET_UI_NAME} INTERFACE)
|
set(CMAKE_AUTOMOC ON)
|
||||||
|
|
||||||
target_link_libraries(${TARGET_UI_NAME} INTERFACE
|
set(RELATIVE_HDRS)
|
||||||
|
foreach (_file ${HDRS})
|
||||||
|
file(RELATIVE_PATH _relPath "${SRC_DIR}" "${_file}")
|
||||||
|
list(APPEND RELATIVE_HDRS "${_relPath}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set(RELATIVE_SRCS)
|
||||||
|
foreach (_file ${SRCS})
|
||||||
|
file(RELATIVE_PATH _relPath "${SRC_DIR}" "${_file}")
|
||||||
|
list(APPEND RELATIVE_SRCS "${_relPath}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
add_files(UI_FILES ${RELATIVE_HDRS} ${RELATIVE_SRCS})
|
||||||
|
|
||||||
|
add_library(${TARGET_UI_NAME} STATIC ${UI_FILES})
|
||||||
|
create_source_groups(${UI_FILES})
|
||||||
|
|
||||||
|
set_target_properties(${TARGET_UI_NAME} PROPERTIES
|
||||||
|
AUTOMOC ON
|
||||||
|
CXX_STANDARD 17
|
||||||
|
)
|
||||||
|
set_property(TARGET ${TARGET_UI_NAME} PROPERTY INCLUDE_DIRECTORIES
|
||||||
|
"${TARGET_UI_INCLUDE_DIRS}"
|
||||||
|
"${TARGET_LIB_INCLUDE_DIRS}"
|
||||||
|
"${LIB_INCLUDE_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(${TARGET_UI_NAME}
|
||||||
${TARGET_LIB_NAME}
|
${TARGET_LIB_NAME}
|
||||||
Qt5::Widgets
|
Qt5::Widgets
|
||||||
${OPENGL_LIBRARIES}
|
${OPENGL_LIBRARIES}
|
||||||
@@ -89,11 +117,13 @@ target_link_libraries(${TARGET_UI_NAME} INTERFACE
|
|||||||
Qt5::Charts
|
Qt5::Charts
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(${TARGET_UI_NAME} INTERFACE
|
set(CMAKE_AUTOMOC OFF)
|
||||||
"${TARGET_UI_INCLUDE_DIRS}"
|
|
||||||
"${TARGET_LIB_INCLUDE_DIRS}"
|
unset(UI_FILES)
|
||||||
"${LIB_INCLUDE_PATH}"
|
unset(RELATIVE_HDRS)
|
||||||
)
|
unset(RELATIVE_SRCS)
|
||||||
|
unset(HDRS)
|
||||||
|
unset(SRCS)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -145,6 +175,9 @@ set_property(TARGET ${TARGET_APP_NAME} PROPERTY CXX_STANDARD 17)
|
|||||||
set_target_properties(${TARGET_APP_NAME} PROPERTIES
|
set_target_properties(${TARGET_APP_NAME} PROPERTIES
|
||||||
VS_DEBUGGER_WORKING_DIRECTORY "${OUTPUT_ROOT_PATH}/$(Configuration)/app/"
|
VS_DEBUGGER_WORKING_DIRECTORY "${OUTPUT_ROOT_PATH}/$(Configuration)/app/"
|
||||||
)
|
)
|
||||||
|
target_compile_definitions(${TARGET_APP_NAME} PRIVATE
|
||||||
|
DOTA_FACTORY_CONFIG_DIR="${CMAKE_SOURCE_DIR}/bin/config"
|
||||||
|
)
|
||||||
target_link_libraries(${TARGET_APP_NAME} ${TARGET_UI_NAME})
|
target_link_libraries(${TARGET_APP_NAME} ${TARGET_UI_NAME})
|
||||||
|
|
||||||
unset(APP_FILES)
|
unset(APP_FILES)
|
||||||
|
|||||||
@@ -12,5 +12,3 @@ SET(SRCS
|
|||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,13 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
|
||||||
|
#include "ConfigLoader.h"
|
||||||
#include "ConsoleLogger.h"
|
#include "ConsoleLogger.h"
|
||||||
#include "logging.h"
|
#include "logging.h"
|
||||||
#include "LogManager.h"
|
#include "LogManager.h"
|
||||||
|
#include "MainWindow.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "VisualsLoader.h"
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
@@ -28,6 +32,13 @@ int main(int argc, char *argv[])
|
|||||||
QDir().mkdir(dataDir.dirName());
|
QDir().mkdir(dataDir.dirName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GameConfig config = ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
||||||
|
VisualsConfig visuals = VisualsLoader::load(std::string(DOTA_FACTORY_CONFIG_DIR) + "/visuals.toml");
|
||||||
|
std::unique_ptr<Simulation> sim = std::make_unique<Simulation>(config);
|
||||||
|
|
||||||
|
MainWindow window(sim.get(), &config, &visuals);
|
||||||
|
window.show();
|
||||||
|
|
||||||
const int ret = application.exec();
|
const int ret = application.exec();
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|||||||
@@ -389,6 +389,25 @@ bool Simulation::isBlueprintUnlocked(const std::string& shipId) const
|
|||||||
return it->second.unlocked;
|
return it->second.unlocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EntityId Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
|
||||||
|
{
|
||||||
|
int cost = 0;
|
||||||
|
for (const BuildingDef& def : m_config.buildings.buildings)
|
||||||
|
{
|
||||||
|
if (def.type == type)
|
||||||
|
{
|
||||||
|
cost = def.cost;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_buildingBlocksStock < cost)
|
||||||
|
{
|
||||||
|
return kInvalidEntityId;
|
||||||
|
}
|
||||||
|
m_buildingBlocksStock -= cost;
|
||||||
|
return m_buildingSystem->place(type, anchor, rotation, m_currentTick);
|
||||||
|
}
|
||||||
|
|
||||||
BuildingSystem& Simulation::buildings()
|
BuildingSystem& Simulation::buildings()
|
||||||
{
|
{
|
||||||
return *m_buildingSystem;
|
return *m_buildingSystem;
|
||||||
|
|||||||
@@ -6,11 +6,15 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "BlueprintDropEvent.h"
|
#include "BlueprintDropEvent.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
#include "EntityId.h"
|
#include "EntityId.h"
|
||||||
#include "FireEvent.h"
|
#include "FireEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "Rotation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
@@ -44,6 +48,10 @@ public:
|
|||||||
int blueprintLevel(const std::string& shipId) const;
|
int blueprintLevel(const std::string& shipId) const;
|
||||||
bool isBlueprintUnlocked(const std::string& shipId) const;
|
bool isBlueprintUnlocked(const std::string& shipId) const;
|
||||||
|
|
||||||
|
// Checks affordability, deducts building blocks, and places the building.
|
||||||
|
// Returns the new entity id, or kInvalidEntityId if blocks are insufficient.
|
||||||
|
EntityId tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation);
|
||||||
|
|
||||||
BuildingSystem& buildings();
|
BuildingSystem& buildings();
|
||||||
const BuildingSystem& buildings() const;
|
const BuildingSystem& buildings() const;
|
||||||
BeltSystem& belts();
|
BeltSystem& belts();
|
||||||
|
|||||||
129
src/ui/BuildButtonGrid.cpp
Normal file
129
src/ui/BuildButtonGrid.cpp
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#include "BuildButtonGrid.h"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSignalMapper>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
QString displayName(const std::string& id)
|
||||||
|
{
|
||||||
|
QString result;
|
||||||
|
bool nextUpper = true;
|
||||||
|
for (char c : id)
|
||||||
|
{
|
||||||
|
if (c == '_')
|
||||||
|
{
|
||||||
|
result += ' ';
|
||||||
|
nextUpper = true;
|
||||||
|
}
|
||||||
|
else if (nextUpper)
|
||||||
|
{
|
||||||
|
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
nextUpper = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_config(config)
|
||||||
|
, m_activeIndex(-1)
|
||||||
|
{
|
||||||
|
QGridLayout* layout = new QGridLayout(this);
|
||||||
|
layout->setSpacing(4);
|
||||||
|
layout->setContentsMargins(4, 4, 4, 4);
|
||||||
|
|
||||||
|
QSignalMapper* mapper = new QSignalMapper(this);
|
||||||
|
int col = 0;
|
||||||
|
int row = 0;
|
||||||
|
const int kCols = 3;
|
||||||
|
|
||||||
|
for (const BuildingDef& def : config->buildings.buildings)
|
||||||
|
{
|
||||||
|
if (!def.playerPlaceable)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_types.push_back(def.type);
|
||||||
|
m_costs[def.type] = def.cost;
|
||||||
|
|
||||||
|
const QString label = displayName(def.id)
|
||||||
|
+ "\n" + QString::number(def.cost) + " Blocks";
|
||||||
|
QPushButton* btn = new QPushButton(label, this);
|
||||||
|
btn->setCheckable(true);
|
||||||
|
btn->setFixedHeight(48);
|
||||||
|
layout->addWidget(btn, row, col);
|
||||||
|
|
||||||
|
const int idx = static_cast<int>(m_buttons.size());
|
||||||
|
m_buttons.push_back(btn);
|
||||||
|
mapper->setMapping(btn, idx);
|
||||||
|
connect(btn, SIGNAL(clicked()), mapper, SLOT(map()));
|
||||||
|
|
||||||
|
++col;
|
||||||
|
if (col >= kCols)
|
||||||
|
{
|
||||||
|
col = 0;
|
||||||
|
++row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connect(mapper, SIGNAL(mapped(int)), this, SLOT(onBuildButton(int)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonGrid::updateAffordability(int buildingBlocks)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i < m_buttons.size(); ++i)
|
||||||
|
{
|
||||||
|
const BuildingType type = m_types[i];
|
||||||
|
const std::map<BuildingType, int>::const_iterator it = m_costs.find(type);
|
||||||
|
const int cost = (it != m_costs.end()) ? it->second : 0;
|
||||||
|
m_buttons[i]->setEnabled(buildingBlocks >= cost || m_activeIndex == static_cast<int>(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonGrid::clearActiveButton()
|
||||||
|
{
|
||||||
|
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_buttons.size()))
|
||||||
|
{
|
||||||
|
m_buttons[static_cast<std::size_t>(m_activeIndex)]->setChecked(false);
|
||||||
|
}
|
||||||
|
m_activeIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonGrid::onBuildButton(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= static_cast<int>(m_buttons.size()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_activeIndex == index)
|
||||||
|
{
|
||||||
|
clearActiveButton();
|
||||||
|
emit builderModeExited();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_buttons.size()))
|
||||||
|
{
|
||||||
|
m_buttons[static_cast<std::size_t>(m_activeIndex)]->setChecked(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_activeIndex = index;
|
||||||
|
m_buttons[static_cast<std::size_t>(index)]->setChecked(true);
|
||||||
|
emit buildingTypeSelected(m_types[static_cast<std::size_t>(index)]);
|
||||||
|
}
|
||||||
36
src/ui/BuildButtonGrid.h
Normal file
36
src/ui/BuildButtonGrid.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
|
class BuildButtonGrid : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
BuildButtonGrid(const GameConfig* config, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void updateAffordability(int buildingBlocks);
|
||||||
|
void clearActiveButton();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void buildingTypeSelected(BuildingType type);
|
||||||
|
void builderModeExited();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onBuildButton(int index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const GameConfig* m_config;
|
||||||
|
std::vector<BuildingType> m_types;
|
||||||
|
std::vector<QPushButton*> m_buttons;
|
||||||
|
std::map<BuildingType, int> m_costs;
|
||||||
|
int m_activeIndex;
|
||||||
|
};
|
||||||
@@ -1,7 +1,22 @@
|
|||||||
# UI source files are listed here as they are added.
|
SET(HDRS
|
||||||
# Append headers and sources to HDRS and SRCS in PARENT_SCOPE,
|
${HDRS}
|
||||||
# following the same pattern used by src/lib/.
|
${CMAKE_CURRENT_SOURCE_DIR}/VisualsConfig.h
|
||||||
#
|
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.h
|
||||||
# When this directory has actual sources, the parent CMakeLists.txt
|
${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.h
|
||||||
# must be updated to convert DotaFactory_ui from an INTERFACE library
|
${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.h
|
||||||
# to a regular static library (and enable AUTOMOC on it).
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
|
|
||||||
|
SET(SRCS
|
||||||
|
${SRCS}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/MainWindow.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
|
|||||||
982
src/ui/GameWorldView.cpp
Normal file
982
src/ui/GameWorldView.cpp
Normal file
@@ -0,0 +1,982 @@
|
|||||||
|
#include "GameWorldView.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPen>
|
||||||
|
#include <QPolygonF>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingSystem.h"
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "Scrap.h"
|
||||||
|
#include "ScrapSystem.h"
|
||||||
|
#include "Ship.h"
|
||||||
|
#include "ShipSystem.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
Rotation rotateClockwise(Rotation r)
|
||||||
|
{
|
||||||
|
switch (r)
|
||||||
|
{
|
||||||
|
case Rotation::North: return Rotation::East;
|
||||||
|
case Rotation::East: return Rotation::South;
|
||||||
|
case Rotation::South: return Rotation::West;
|
||||||
|
case Rotation::West: return Rotation::North;
|
||||||
|
}
|
||||||
|
return Rotation::East;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rotation rotateCounterClockwise(Rotation r)
|
||||||
|
{
|
||||||
|
switch (r)
|
||||||
|
{
|
||||||
|
case Rotation::North: return Rotation::West;
|
||||||
|
case Rotation::East: return Rotation::North;
|
||||||
|
case Rotation::South: return Rotation::East;
|
||||||
|
case Rotation::West: return Rotation::South;
|
||||||
|
}
|
||||||
|
return Rotation::East;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipRole shipRole(const Ship& ship)
|
||||||
|
{
|
||||||
|
if (ship.isEnemy) { return ShipRole::Enemy; }
|
||||||
|
if (ship.cargo.has_value()) { return ShipRole::Salvage; }
|
||||||
|
if (ship.repairTool.has_value()) { return ShipRole::Repair; }
|
||||||
|
return ShipRole::PlayerCombat;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString toDisplayName(const std::string& id)
|
||||||
|
{
|
||||||
|
QString result;
|
||||||
|
bool nextUpper = true;
|
||||||
|
for (char c : id)
|
||||||
|
{
|
||||||
|
if (c == '_')
|
||||||
|
{
|
||||||
|
result += ' ';
|
||||||
|
nextUpper = true;
|
||||||
|
}
|
||||||
|
else if (nextUpper)
|
||||||
|
{
|
||||||
|
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
nextUpper = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
||||||
|
const VisualsConfig* visuals, QWidget* parent)
|
||||||
|
: QOpenGLWidget(parent)
|
||||||
|
, m_sim(sim)
|
||||||
|
, m_config(config)
|
||||||
|
, m_visuals(visuals)
|
||||||
|
, m_wallMs(0)
|
||||||
|
, m_gameSpeedMultiplier(1.0)
|
||||||
|
, m_prevNonZeroSpeed(1.0)
|
||||||
|
, m_scrollXTiles(0.0f)
|
||||||
|
, m_ghostRotation(Rotation::East)
|
||||||
|
, m_ghostValid(false)
|
||||||
|
, m_dragging(false)
|
||||||
|
, m_demolishMode(false)
|
||||||
|
, m_demolishHoverId(kInvalidEntityId)
|
||||||
|
, m_boxSelecting(false)
|
||||||
|
, m_scrollLeft(false)
|
||||||
|
, m_scrollRight(false)
|
||||||
|
, m_gameOverShown(false)
|
||||||
|
{
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
setMouseTracking(true);
|
||||||
|
|
||||||
|
m_renderTimer = new QTimer(this);
|
||||||
|
m_renderTimer->setInterval(16);
|
||||||
|
connect(m_renderTimer, SIGNAL(timeout()), this, SLOT(onFrame()));
|
||||||
|
m_renderTimer->start();
|
||||||
|
m_frameTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::initializeGL()
|
||||||
|
{
|
||||||
|
// QPainter handles all rendering; no custom GL state needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::onFrame()
|
||||||
|
{
|
||||||
|
const qint64 elapsed = m_frameTimer.restart();
|
||||||
|
m_wallMs += elapsed;
|
||||||
|
|
||||||
|
// Advance simulation
|
||||||
|
{
|
||||||
|
const int ticks = m_tickDriver.advance(
|
||||||
|
static_cast<double>(elapsed), m_gameSpeedMultiplier);
|
||||||
|
for (int i = 0; i < ticks; ++i)
|
||||||
|
{
|
||||||
|
m_sim->tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain fire events → active beams
|
||||||
|
{
|
||||||
|
const std::vector<FireEvent> fires = m_sim->drainFireEvents();
|
||||||
|
for (const FireEvent& fe : fires)
|
||||||
|
{
|
||||||
|
ActiveBeam beam;
|
||||||
|
beam.event = fe;
|
||||||
|
beam.emittedWallMs = m_wallMs;
|
||||||
|
m_activeBeams.push_back(beam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain blueprint drop events → toasts
|
||||||
|
{
|
||||||
|
const std::vector<BlueprintDropEvent> drops =
|
||||||
|
m_sim->drainBlueprintDropEvents();
|
||||||
|
for (const BlueprintDropEvent& ev : drops)
|
||||||
|
{
|
||||||
|
const QString shipName = toDisplayName(ev.blueprintId);
|
||||||
|
ToastEntry toast;
|
||||||
|
if (ev.wasNewUnlock)
|
||||||
|
{
|
||||||
|
toast.text = "Blueprint unlocked: " + shipName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toast.text = shipName + " production level \u2192 "
|
||||||
|
+ QString::number(ev.newLevel);
|
||||||
|
}
|
||||||
|
toast.createdWallMs = m_wallMs;
|
||||||
|
m_toasts.push_back(toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire old beams
|
||||||
|
{
|
||||||
|
std::vector<ActiveBeam> live;
|
||||||
|
for (const ActiveBeam& b : m_activeBeams)
|
||||||
|
{
|
||||||
|
if (m_wallMs - b.emittedWallMs < kBeamLifetimeMs)
|
||||||
|
{
|
||||||
|
live.push_back(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_activeBeams = std::move(live);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire old toasts
|
||||||
|
{
|
||||||
|
std::vector<ToastEntry> live;
|
||||||
|
for (const ToastEntry& t : m_toasts)
|
||||||
|
{
|
||||||
|
if (m_wallMs - t.createdWallMs < kToastLifetimeMs)
|
||||||
|
{
|
||||||
|
live.push_back(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_toasts = std::move(live);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply held scroll
|
||||||
|
{
|
||||||
|
const float delta = kScrollSpeedTilesPerSec
|
||||||
|
* static_cast<float>(elapsed) / 1000.0f;
|
||||||
|
if (m_scrollLeft) { m_scrollXTiles -= delta; }
|
||||||
|
if (m_scrollRight) { m_scrollXTiles += delta; }
|
||||||
|
clampScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit state update for header bar / build grid
|
||||||
|
emit stateUpdated(m_sim->currentTick(),
|
||||||
|
m_sim->buildingBlocksStock(),
|
||||||
|
m_gameSpeedMultiplier);
|
||||||
|
|
||||||
|
// Game over check
|
||||||
|
if (m_sim->isGameOver() && !m_gameOverShown)
|
||||||
|
{
|
||||||
|
m_gameOverShown = true;
|
||||||
|
m_gameSpeedMultiplier = 0.0;
|
||||||
|
emit gameOver();
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::paintGL()
|
||||||
|
{
|
||||||
|
QPainter painter(this);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||||
|
painter.translate(-static_cast<qreal>(m_scrollXTiles) * static_cast<qreal>(tilePx()), 0.0);
|
||||||
|
|
||||||
|
drawTiles(painter);
|
||||||
|
drawBuildings(painter);
|
||||||
|
drawBeltItems(painter);
|
||||||
|
drawScrap(painter);
|
||||||
|
drawShips(painter);
|
||||||
|
drawBeams(painter);
|
||||||
|
drawOverlays(painter);
|
||||||
|
drawScreenSpace(painter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Coordinate helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
float GameWorldView::tilePx() const
|
||||||
|
{
|
||||||
|
return 20.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float GameWorldView::viewportWidthTiles() const
|
||||||
|
{
|
||||||
|
return static_cast<float>(width()) / tilePx();
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointF GameWorldView::worldToWidget(QVector2D worldPos) const
|
||||||
|
{
|
||||||
|
return QPointF(
|
||||||
|
static_cast<qreal>((worldPos.x() - m_scrollXTiles) * tilePx()),
|
||||||
|
static_cast<qreal>(worldPos.y() * tilePx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointF GameWorldView::tileToWidget(QPoint tile) const
|
||||||
|
{
|
||||||
|
return worldToWidget(QVector2D(static_cast<float>(tile.x()),
|
||||||
|
static_cast<float>(tile.y())));
|
||||||
|
}
|
||||||
|
|
||||||
|
QPoint GameWorldView::widgetToTile(QPoint widgetPt) const
|
||||||
|
{
|
||||||
|
const float wx = static_cast<float>(widgetPt.x()) / tilePx() + m_scrollXTiles;
|
||||||
|
const float wy = static_cast<float>(widgetPt.y()) / tilePx();
|
||||||
|
return QPoint(static_cast<int>(std::floor(wx)), static_cast<int>(std::floor(wy)));
|
||||||
|
}
|
||||||
|
|
||||||
|
QRectF GameWorldView::tileRect(QPoint tile) const
|
||||||
|
{
|
||||||
|
const QPointF tl = tileToWidget(tile);
|
||||||
|
return QRectF(tl.x(), tl.y(),
|
||||||
|
static_cast<qreal>(tilePx()), static_cast<qreal>(tilePx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect GameWorldView::viewportRect() const
|
||||||
|
{
|
||||||
|
const int left = static_cast<int>(std::floor(m_scrollXTiles)) - 1;
|
||||||
|
const int top = 0;
|
||||||
|
const int right = static_cast<int>(std::ceil(m_scrollXTiles + viewportWidthTiles())) + 1;
|
||||||
|
const int bottom = m_config->world.heightTiles;
|
||||||
|
return QRect(left, top, right - left, bottom - top);
|
||||||
|
}
|
||||||
|
|
||||||
|
float GameWorldView::asteroidLeftEdge() const
|
||||||
|
{
|
||||||
|
float leftX = -static_cast<float>(m_config->world.regions.asteroidWidth);
|
||||||
|
for (const Building& b : m_sim->buildings().allBuildings())
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : b.bodyCells)
|
||||||
|
{
|
||||||
|
if (static_cast<float>(cell.x()) < leftX)
|
||||||
|
{
|
||||||
|
leftX = static_cast<float>(cell.x());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return leftX;
|
||||||
|
}
|
||||||
|
|
||||||
|
float GameWorldView::enemyStationRightEdge() const
|
||||||
|
{
|
||||||
|
float rightX = static_cast<float>(m_config->world.regions.playerBufferWidth
|
||||||
|
+ m_config->world.regions.contestZoneWidth);
|
||||||
|
for (const Building& b : m_sim->buildings().allBuildings())
|
||||||
|
{
|
||||||
|
if (b.type == BuildingType::EnemyDefenceStation)
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : b.bodyCells)
|
||||||
|
{
|
||||||
|
const float cx = static_cast<float>(cell.x() + 1);
|
||||||
|
if (cx > rightX) { rightX = cx; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rightX;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::clampScroll()
|
||||||
|
{
|
||||||
|
const float leftBound = asteroidLeftEdge();
|
||||||
|
const float rightBound = enemyStationRightEdge() - viewportWidthTiles();
|
||||||
|
m_scrollXTiles = std::max(leftBound, std::min(m_scrollXTiles, rightBound));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Placement helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const BuildingDef* GameWorldView::findBuildingDef(BuildingType type) const
|
||||||
|
{
|
||||||
|
for (const BuildingDef& def : m_config->buildings.buildings)
|
||||||
|
{
|
||||||
|
if (def.type == type) { return &def; }
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
|
||||||
|
Rotation rot) const
|
||||||
|
{
|
||||||
|
const BuildingDef* def = findBuildingDef(type);
|
||||||
|
if (!def) { return false; }
|
||||||
|
|
||||||
|
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rot);
|
||||||
|
|
||||||
|
for (const QPoint& relCell : parsed.bodyCells)
|
||||||
|
{
|
||||||
|
const QPoint worldCell = anchor + relCell;
|
||||||
|
|
||||||
|
// Terrain check: S cells must be space (x >= 0), A cells must be asteroid (x < 0)
|
||||||
|
bool isShipDock = false;
|
||||||
|
for (const QPoint& dock : parsed.shipDockCells)
|
||||||
|
{
|
||||||
|
if (dock == relCell)
|
||||||
|
{
|
||||||
|
isShipDock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isShipDock && worldCell.x() < 0) { return false; }
|
||||||
|
if (!isShipDock && worldCell.x() >= 0) { return false; }
|
||||||
|
|
||||||
|
// Occupancy check
|
||||||
|
if (m_sim->buildings().isTileOccupied(worldCell)) { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityId GameWorldView::buildingAtTile(QPoint tile) const
|
||||||
|
{
|
||||||
|
for (const Building& b : m_sim->buildings().allBuildings())
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : b.bodyCells)
|
||||||
|
{
|
||||||
|
if (cell == tile) { return b.id; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kInvalidEntityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QVector2D> GameWorldView::entityPosition(EntityId id) const
|
||||||
|
{
|
||||||
|
const Ship* ship = m_sim->ships().findShip(id);
|
||||||
|
if (ship)
|
||||||
|
{
|
||||||
|
return ship->position;
|
||||||
|
}
|
||||||
|
const Building* bldg = m_sim->buildings().findBuilding(id);
|
||||||
|
if (bldg)
|
||||||
|
{
|
||||||
|
return QVector2D(
|
||||||
|
bldg->anchor.x() + bldg->footprint.width() * 0.5f,
|
||||||
|
bldg->anchor.y() + bldg->footprint.height() * 0.5f);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::stepSpeed(int delta)
|
||||||
|
{
|
||||||
|
const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 4.0 };
|
||||||
|
const int kCount = 5;
|
||||||
|
int current = 2;
|
||||||
|
for (int i = 0; i < kCount; ++i)
|
||||||
|
{
|
||||||
|
if (std::abs(kSpeeds[i] - m_gameSpeedMultiplier) < 0.001)
|
||||||
|
{
|
||||||
|
current = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int next = std::max(0, std::min(kCount - 1, current + delta));
|
||||||
|
setGameSpeed(kSpeeds[next]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::placeAtTile(QPoint tile)
|
||||||
|
{
|
||||||
|
if (!m_builderType.has_value()) { return; }
|
||||||
|
const BuildingType type = *m_builderType;
|
||||||
|
|
||||||
|
if (!isValidPlacement(type, tile, m_ghostRotation)) { return; }
|
||||||
|
|
||||||
|
if (type == BuildingType::Belt)
|
||||||
|
{
|
||||||
|
if (m_beltDragTiles.count(tile) > 0) { return; }
|
||||||
|
if (!m_sim->buildings().isTileOccupied(tile))
|
||||||
|
{
|
||||||
|
const EntityId id = m_sim->tryPlaceBuilding(
|
||||||
|
type, tile, m_ghostRotation);
|
||||||
|
if (id != kInvalidEntityId)
|
||||||
|
{
|
||||||
|
m_beltDragTiles.insert(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == BuildingType::Splitter)
|
||||||
|
{
|
||||||
|
if (!m_sim->buildings().isTileOccupied(tile))
|
||||||
|
{
|
||||||
|
m_sim->tryPlaceBuilding(type, tile, m_ghostRotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_sim->tryPlaceBuilding(type, tile, m_ghostRotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Rendering
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void GameWorldView::drawTiles(QPainter& painter)
|
||||||
|
{
|
||||||
|
const int leftTile = static_cast<int>(std::floor(m_scrollXTiles)) - 1;
|
||||||
|
const int rightTile = leftTile + static_cast<int>(std::ceil(viewportWidthTiles())) + 2;
|
||||||
|
const int bottomTile = m_config->world.heightTiles;
|
||||||
|
|
||||||
|
painter.setPen(Qt::NoPen);
|
||||||
|
for (int x = leftTile; x <= rightTile; ++x)
|
||||||
|
{
|
||||||
|
const QColor& fill = (x < 0)
|
||||||
|
? m_visuals->asteroid.fill
|
||||||
|
: m_visuals->space.fill;
|
||||||
|
for (int y = 0; y < bottomTile; ++y)
|
||||||
|
{
|
||||||
|
painter.fillRect(tileRect(QPoint(x, y)), fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::drawBuildings(QPainter& painter)
|
||||||
|
{
|
||||||
|
for (const Building& b : m_sim->buildings().allBuildings())
|
||||||
|
{
|
||||||
|
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||||
|
m_visuals->buildings.find(b.type);
|
||||||
|
if (it == m_visuals->buildings.end()) { continue; }
|
||||||
|
const BuildingVisuals& bv = it->second;
|
||||||
|
|
||||||
|
painter.setPen(Qt::NoPen);
|
||||||
|
for (const QPoint& cell : b.bodyCells)
|
||||||
|
{
|
||||||
|
painter.fillRect(tileRect(cell), bv.fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPointF tl = tileToWidget(b.anchor);
|
||||||
|
const QRectF bboxRect(tl.x(), tl.y(),
|
||||||
|
b.footprint.width() * static_cast<qreal>(tilePx()),
|
||||||
|
b.footprint.height() * static_cast<qreal>(tilePx()));
|
||||||
|
|
||||||
|
painter.setPen(QPen(bv.outline, 1));
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawRect(bboxRect);
|
||||||
|
|
||||||
|
if (!bv.glyph.isEmpty())
|
||||||
|
{
|
||||||
|
painter.setPen(bv.outline);
|
||||||
|
painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool selected = false;
|
||||||
|
for (EntityId selId : m_selectedIds)
|
||||||
|
{
|
||||||
|
if (selId == b.id) { selected = true; break; }
|
||||||
|
}
|
||||||
|
if (selected)
|
||||||
|
{
|
||||||
|
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawRect(bboxRect.adjusted(-1, -1, 1, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.setOpacity(0.5);
|
||||||
|
for (const ConstructionSite& s : m_sim->buildings().allSites())
|
||||||
|
{
|
||||||
|
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||||
|
m_visuals->buildings.find(s.type);
|
||||||
|
if (it == m_visuals->buildings.end()) { continue; }
|
||||||
|
const BuildingVisuals& bv = it->second;
|
||||||
|
|
||||||
|
for (const QPoint& cell : s.bodyCells)
|
||||||
|
{
|
||||||
|
painter.fillRect(tileRect(cell), bv.fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPointF tl = tileToWidget(s.anchor);
|
||||||
|
const QRectF bboxRect(tl.x(), tl.y(),
|
||||||
|
s.footprint.width() * static_cast<qreal>(tilePx()),
|
||||||
|
s.footprint.height() * static_cast<qreal>(tilePx()));
|
||||||
|
painter.setPen(QPen(bv.outline, 1, Qt::DashLine));
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawRect(bboxRect);
|
||||||
|
}
|
||||||
|
painter.setOpacity(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::drawBeltItems(QPainter& painter)
|
||||||
|
{
|
||||||
|
const float halfPx = tilePx() * 0.5f * 0.5f;
|
||||||
|
const QRect vr = viewportRect();
|
||||||
|
|
||||||
|
m_sim->belts().forEachVisualItem(vr, [&](const VisualItem& vi)
|
||||||
|
{
|
||||||
|
const std::map<std::string, ItemVisuals>::const_iterator it =
|
||||||
|
m_visuals->items.find(vi.type.id);
|
||||||
|
if (it == m_visuals->items.end()) { return; }
|
||||||
|
|
||||||
|
const QPointF center = worldToWidget(
|
||||||
|
QVector2D(static_cast<float>(vi.worldPos.x()),
|
||||||
|
static_cast<float>(vi.worldPos.y())));
|
||||||
|
const QRectF rect(center.x() - halfPx, center.y() - halfPx,
|
||||||
|
halfPx * 2, halfPx * 2);
|
||||||
|
painter.fillRect(rect, it->second.fill);
|
||||||
|
painter.setPen(QPen(it->second.outline, 1));
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawRect(rect);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::drawScrap(QPainter& painter)
|
||||||
|
{
|
||||||
|
const float r = tilePx() * 0.2f;
|
||||||
|
for (const Scrap& scrap : m_sim->scraps().allScraps())
|
||||||
|
{
|
||||||
|
const QPointF center = worldToWidget(scrap.position);
|
||||||
|
painter.setBrush(QColor(128, 110, 90));
|
||||||
|
painter.setPen(QPen(QColor(50, 40, 30), 1));
|
||||||
|
painter.drawEllipse(center,
|
||||||
|
static_cast<qreal>(r), static_cast<qreal>(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::drawShips(QPainter& painter)
|
||||||
|
{
|
||||||
|
for (const Ship& ship : m_sim->ships().allShips())
|
||||||
|
{
|
||||||
|
const ShipRole role = shipRole(ship);
|
||||||
|
const std::map<ShipRole, ShipVisuals>::const_iterator it =
|
||||||
|
m_visuals->ships.find(role);
|
||||||
|
if (it == m_visuals->ships.end()) { continue; }
|
||||||
|
|
||||||
|
const QPointF center = worldToWidget(ship.position);
|
||||||
|
const QVector2D vel = ship.velocity;
|
||||||
|
const QVector2D dir = (vel.length() > 0.0001f)
|
||||||
|
? vel.normalized()
|
||||||
|
: QVector2D(1.0f, 0.0f);
|
||||||
|
const QVector2D perp(-dir.y(), dir.x());
|
||||||
|
|
||||||
|
const float fwd = tilePx() * 0.45f;
|
||||||
|
const float side = tilePx() * 0.25f;
|
||||||
|
|
||||||
|
QPolygonF tri;
|
||||||
|
tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd),
|
||||||
|
center.y() + static_cast<qreal>(dir.y() * fwd))
|
||||||
|
<< QPointF(center.x() + static_cast<qreal>(perp.x() * side - dir.x() * side),
|
||||||
|
center.y() + static_cast<qreal>(perp.y() * side - dir.y() * side))
|
||||||
|
<< QPointF(center.x() + static_cast<qreal>(-perp.x() * side - dir.x() * side),
|
||||||
|
center.y() + static_cast<qreal>(-perp.y() * side - dir.y() * side));
|
||||||
|
|
||||||
|
painter.setPen(QPen(it->second.outline, 1));
|
||||||
|
painter.setBrush(it->second.fill);
|
||||||
|
painter.drawPolygon(tri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::drawBeams(QPainter& painter)
|
||||||
|
{
|
||||||
|
painter.setPen(QPen(m_visuals->beams.color, m_visuals->beams.widthPx));
|
||||||
|
for (const ActiveBeam& beam : m_activeBeams)
|
||||||
|
{
|
||||||
|
const std::optional<QVector2D> shooterPos = entityPosition(beam.event.shooter);
|
||||||
|
const std::optional<QVector2D> targetPos = entityPosition(beam.event.target);
|
||||||
|
if (!shooterPos.has_value() || !targetPos.has_value()) { continue; }
|
||||||
|
painter.drawLine(worldToWidget(*shooterPos), worldToWidget(*targetPos));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::drawOverlays(QPainter& painter)
|
||||||
|
{
|
||||||
|
// Builder-mode ghost
|
||||||
|
if (m_builderType.has_value())
|
||||||
|
{
|
||||||
|
const BuildingDef* def = findBuildingDef(*m_builderType);
|
||||||
|
if (def)
|
||||||
|
{
|
||||||
|
const ParsedSurfaceMask parsed =
|
||||||
|
parseSurfaceMask(def->surfaceMask, m_ghostRotation);
|
||||||
|
const QColor& ghostColor = m_ghostValid
|
||||||
|
? m_visuals->overlays.ghostValid
|
||||||
|
: m_visuals->overlays.ghostInvalid;
|
||||||
|
for (const QPoint& cell : parsed.bodyCells)
|
||||||
|
{
|
||||||
|
painter.fillRect(tileRect(m_ghostTile + cell), ghostColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demolish hover tint
|
||||||
|
if (m_demolishMode && m_demolishHoverId != kInvalidEntityId)
|
||||||
|
{
|
||||||
|
const Building* b = m_sim->buildings().findBuilding(m_demolishHoverId);
|
||||||
|
if (b)
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : b->bodyCells)
|
||||||
|
{
|
||||||
|
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box-select rectangle
|
||||||
|
if (m_boxSelecting)
|
||||||
|
{
|
||||||
|
const QPoint tl(std::min(m_boxStartTile.x(), m_boxCurrentTile.x()),
|
||||||
|
std::min(m_boxStartTile.y(), m_boxCurrentTile.y()));
|
||||||
|
const QPoint br(std::max(m_boxStartTile.x(), m_boxCurrentTile.x()) + 1,
|
||||||
|
std::max(m_boxStartTile.y(), m_boxCurrentTile.y()) + 1);
|
||||||
|
const QRectF selRect(tileToWidget(tl), tileToWidget(br));
|
||||||
|
painter.setPen(QPen(m_visuals->overlays.selectionRect, 1));
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawRect(selRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::drawScreenSpace(QPainter& painter)
|
||||||
|
{
|
||||||
|
painter.resetTransform();
|
||||||
|
|
||||||
|
const int margin = 8;
|
||||||
|
const int toastW = 320;
|
||||||
|
const int toastH = 36;
|
||||||
|
const int spacing = 4;
|
||||||
|
|
||||||
|
QFont toastFont = painter.font();
|
||||||
|
toastFont.setPointSize(m_visuals->toast.fontSize);
|
||||||
|
painter.setFont(toastFont);
|
||||||
|
|
||||||
|
int y = margin;
|
||||||
|
for (const ToastEntry& toast : m_toasts)
|
||||||
|
{
|
||||||
|
const qint64 age = m_wallMs - toast.createdWallMs;
|
||||||
|
double opacity = 1.0;
|
||||||
|
if (age > kToastFadeStartMs)
|
||||||
|
{
|
||||||
|
opacity = 1.0 - static_cast<double>(age - kToastFadeStartMs)
|
||||||
|
/ static_cast<double>(kToastLifetimeMs - kToastFadeStartMs);
|
||||||
|
opacity = std::max(0.0, opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.setOpacity(opacity);
|
||||||
|
const int x = width() - toastW - margin;
|
||||||
|
const QRect toastRect(x, y, toastW, toastH);
|
||||||
|
painter.fillRect(toastRect, m_visuals->toast.bg);
|
||||||
|
painter.setPen(m_visuals->toast.fg);
|
||||||
|
painter.drawText(toastRect.adjusted(8, 0, -8, 0),
|
||||||
|
Qt::AlignVCenter | Qt::AlignLeft, toast.text);
|
||||||
|
painter.setOpacity(1.0);
|
||||||
|
y += toastH + spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Input
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void GameWorldView::keyPressEvent(QKeyEvent* event)
|
||||||
|
{
|
||||||
|
if (event->isAutoRepeat())
|
||||||
|
{
|
||||||
|
QOpenGLWidget::keyPressEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event->key())
|
||||||
|
{
|
||||||
|
case Qt::Key_A:
|
||||||
|
m_scrollLeft = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_D:
|
||||||
|
m_scrollRight = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_Space:
|
||||||
|
if (m_gameSpeedMultiplier > 0.0)
|
||||||
|
{
|
||||||
|
m_prevNonZeroSpeed = m_gameSpeedMultiplier;
|
||||||
|
setGameSpeed(0.0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setGameSpeed(m_prevNonZeroSpeed);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt::Key_W:
|
||||||
|
stepSpeed(+1);
|
||||||
|
break;
|
||||||
|
case Qt::Key_S:
|
||||||
|
stepSpeed(-1);
|
||||||
|
break;
|
||||||
|
case Qt::Key_E:
|
||||||
|
if (m_builderType.has_value())
|
||||||
|
{
|
||||||
|
m_ghostRotation = rotateClockwise(m_ghostRotation);
|
||||||
|
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt::Key_Q:
|
||||||
|
if (m_builderType.has_value())
|
||||||
|
{
|
||||||
|
m_ghostRotation = rotateCounterClockwise(m_ghostRotation);
|
||||||
|
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt::Key_Escape:
|
||||||
|
if (m_builderType.has_value())
|
||||||
|
{
|
||||||
|
exitBuilderMode();
|
||||||
|
}
|
||||||
|
else if (m_demolishMode)
|
||||||
|
{
|
||||||
|
m_demolishMode = false;
|
||||||
|
m_demolishHoverId = kInvalidEntityId;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt::Key_Backspace:
|
||||||
|
if (m_demolishMode)
|
||||||
|
{
|
||||||
|
m_demolishMode = false;
|
||||||
|
m_demolishHoverId = kInvalidEntityId;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (m_builderType.has_value()) { exitBuilderMode(); }
|
||||||
|
m_demolishMode = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
QOpenGLWidget::keyPressEvent(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::keyReleaseEvent(QKeyEvent* event)
|
||||||
|
{
|
||||||
|
if (event->isAutoRepeat())
|
||||||
|
{
|
||||||
|
QOpenGLWidget::keyReleaseEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event->key() == Qt::Key_A) { m_scrollLeft = false; }
|
||||||
|
if (event->key() == Qt::Key_D) { m_scrollRight = false; }
|
||||||
|
QOpenGLWidget::keyReleaseEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
if (event->button() != Qt::LeftButton)
|
||||||
|
{
|
||||||
|
if (event->button() == Qt::RightButton && m_builderType.has_value())
|
||||||
|
{
|
||||||
|
exitBuilderMode();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint tile = widgetToTile(event->pos());
|
||||||
|
|
||||||
|
if (m_builderType.has_value())
|
||||||
|
{
|
||||||
|
const BuildingType type = *m_builderType;
|
||||||
|
if (type == BuildingType::Belt || type == BuildingType::Splitter)
|
||||||
|
{
|
||||||
|
m_dragging = true;
|
||||||
|
m_beltDragTiles.clear();
|
||||||
|
placeAtTile(tile);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
placeAtTile(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_demolishMode)
|
||||||
|
{
|
||||||
|
const EntityId hovered = buildingAtTile(tile);
|
||||||
|
if (hovered != kInvalidEntityId)
|
||||||
|
{
|
||||||
|
const Building* b = m_sim->buildings().findBuilding(hovered);
|
||||||
|
if (b && b->type != BuildingType::Hq
|
||||||
|
&& b->type != BuildingType::PlayerDefenceStation)
|
||||||
|
{
|
||||||
|
m_sim->buildings().demolish(hovered);
|
||||||
|
m_demolishHoverId = kInvalidEntityId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const EntityId id = buildingAtTile(tile);
|
||||||
|
if (id != kInvalidEntityId)
|
||||||
|
{
|
||||||
|
if (event->modifiers() & Qt::ControlModifier)
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
std::vector<EntityId> newSel;
|
||||||
|
for (EntityId sel : m_selectedIds)
|
||||||
|
{
|
||||||
|
if (sel == id) { found = true; }
|
||||||
|
else { newSel.push_back(sel); }
|
||||||
|
}
|
||||||
|
if (!found) { newSel.push_back(id); }
|
||||||
|
m_selectedIds = newSel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_selectedIds = { id };
|
||||||
|
}
|
||||||
|
emit selectionChanged(m_selectedIds);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!(event->modifiers() & Qt::ControlModifier))
|
||||||
|
{
|
||||||
|
m_selectedIds.clear();
|
||||||
|
emit selectionChanged(m_selectedIds);
|
||||||
|
}
|
||||||
|
m_boxSelecting = true;
|
||||||
|
m_boxStartTile = tile;
|
||||||
|
m_boxCurrentTile = tile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
const QPoint tile = widgetToTile(event->pos());
|
||||||
|
|
||||||
|
if (m_builderType.has_value())
|
||||||
|
{
|
||||||
|
m_ghostTile = tile;
|
||||||
|
m_ghostValid = isValidPlacement(*m_builderType, tile, m_ghostRotation);
|
||||||
|
|
||||||
|
if (m_dragging)
|
||||||
|
{
|
||||||
|
placeAtTile(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_demolishMode)
|
||||||
|
{
|
||||||
|
m_demolishHoverId = buildingAtTile(tile);
|
||||||
|
}
|
||||||
|
else if (m_boxSelecting)
|
||||||
|
{
|
||||||
|
m_boxCurrentTile = tile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
||||||
|
{
|
||||||
|
if (event->button() != Qt::LeftButton) { return; }
|
||||||
|
|
||||||
|
if (m_dragging)
|
||||||
|
{
|
||||||
|
m_dragging = false;
|
||||||
|
m_beltDragTiles.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_boxSelecting)
|
||||||
|
{
|
||||||
|
m_boxSelecting = false;
|
||||||
|
|
||||||
|
const int x0 = std::min(m_boxStartTile.x(), m_boxCurrentTile.x());
|
||||||
|
const int y0 = std::min(m_boxStartTile.y(), m_boxCurrentTile.y());
|
||||||
|
const int x1 = std::max(m_boxStartTile.x(), m_boxCurrentTile.x());
|
||||||
|
const int y1 = std::max(m_boxStartTile.y(), m_boxCurrentTile.y());
|
||||||
|
|
||||||
|
std::vector<EntityId> boxSel;
|
||||||
|
for (const Building& b : m_sim->buildings().allBuildings())
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : b.bodyCells)
|
||||||
|
{
|
||||||
|
if (cell.x() >= x0 && cell.x() <= x1
|
||||||
|
&& cell.y() >= y0 && cell.y() <= y1)
|
||||||
|
{
|
||||||
|
boxSel.push_back(b.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(event->modifiers() & Qt::ControlModifier))
|
||||||
|
{
|
||||||
|
m_selectedIds = boxSel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (EntityId id : boxSel)
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
for (EntityId sel : m_selectedIds)
|
||||||
|
{
|
||||||
|
if (sel == id) { found = true; break; }
|
||||||
|
}
|
||||||
|
if (!found) { m_selectedIds.push_back(id); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit selectionChanged(m_selectedIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Slots
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void GameWorldView::enterBuilderMode(BuildingType type)
|
||||||
|
{
|
||||||
|
m_builderType = type;
|
||||||
|
m_ghostRotation = Rotation::East;
|
||||||
|
m_ghostValid = false;
|
||||||
|
m_demolishMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::exitBuilderMode()
|
||||||
|
{
|
||||||
|
m_builderType.reset();
|
||||||
|
m_beltDragTiles.clear();
|
||||||
|
m_dragging = false;
|
||||||
|
emit builderModeExited();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::setGameSpeed(double multiplier)
|
||||||
|
{
|
||||||
|
m_gameSpeedMultiplier = multiplier;
|
||||||
|
emit stateUpdated(m_sim->currentTick(),
|
||||||
|
m_sim->buildingBlocksStock(),
|
||||||
|
m_gameSpeedMultiplier);
|
||||||
|
}
|
||||||
148
src/ui/GameWorldView.h
Normal file
148
src/ui/GameWorldView.h
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QOpenGLWidget>
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QRectF>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include "BlueprintDropEvent.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "EntityId.h"
|
||||||
|
#include "FireEvent.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
#include "TickDriver.h"
|
||||||
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
|
class Simulation;
|
||||||
|
class QPainter;
|
||||||
|
|
||||||
|
struct QPointCompare
|
||||||
|
{
|
||||||
|
bool operator()(const QPoint& a, const QPoint& b) const
|
||||||
|
{
|
||||||
|
if (a.x() != b.x()) { return a.x() < b.x(); }
|
||||||
|
return a.y() < b.y();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class GameWorldView : public QOpenGLWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
GameWorldView(Simulation* sim, const GameConfig* config,
|
||||||
|
const VisualsConfig* visuals, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void selectionChanged(const std::vector<EntityId>& ids);
|
||||||
|
void stateUpdated(Tick tick, int blocks, double speed);
|
||||||
|
void gameOver();
|
||||||
|
void builderModeExited();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void enterBuilderMode(BuildingType type);
|
||||||
|
void exitBuilderMode();
|
||||||
|
void setGameSpeed(double multiplier);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void initializeGL() override;
|
||||||
|
void paintGL() override;
|
||||||
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
void keyReleaseEvent(QKeyEvent* event) override;
|
||||||
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent* event) override;
|
||||||
|
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onFrame();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void drawTiles(QPainter& painter);
|
||||||
|
void drawBuildings(QPainter& painter);
|
||||||
|
void drawBeltItems(QPainter& painter);
|
||||||
|
void drawScrap(QPainter& painter);
|
||||||
|
void drawShips(QPainter& painter);
|
||||||
|
void drawBeams(QPainter& painter);
|
||||||
|
void drawOverlays(QPainter& painter);
|
||||||
|
void drawScreenSpace(QPainter& painter);
|
||||||
|
|
||||||
|
float tilePx() const;
|
||||||
|
float viewportWidthTiles() const;
|
||||||
|
QPointF worldToWidget(QVector2D worldPos) const;
|
||||||
|
QPointF tileToWidget(QPoint tile) const;
|
||||||
|
QPoint widgetToTile(QPoint widgetPt) const;
|
||||||
|
QRectF tileRect(QPoint tile) const;
|
||||||
|
QRect viewportRect() const;
|
||||||
|
|
||||||
|
float asteroidLeftEdge() const;
|
||||||
|
float enemyStationRightEdge() const;
|
||||||
|
void clampScroll();
|
||||||
|
|
||||||
|
bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const;
|
||||||
|
const BuildingDef* findBuildingDef(BuildingType type) const;
|
||||||
|
EntityId buildingAtTile(QPoint tile) const;
|
||||||
|
|
||||||
|
std::optional<QVector2D> entityPosition(EntityId id) const;
|
||||||
|
void stepSpeed(int delta);
|
||||||
|
void placeAtTile(QPoint tile);
|
||||||
|
|
||||||
|
struct ActiveBeam
|
||||||
|
{
|
||||||
|
FireEvent event;
|
||||||
|
qint64 emittedWallMs;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToastEntry
|
||||||
|
{
|
||||||
|
QString text;
|
||||||
|
qint64 createdWallMs;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr qint64 kBeamLifetimeMs = 300;
|
||||||
|
static constexpr qint64 kToastLifetimeMs = 4000;
|
||||||
|
static constexpr qint64 kToastFadeStartMs = 3500;
|
||||||
|
static constexpr float kScrollSpeedTilesPerSec = 10.0f;
|
||||||
|
|
||||||
|
Simulation* m_sim;
|
||||||
|
const GameConfig* m_config;
|
||||||
|
const VisualsConfig* m_visuals;
|
||||||
|
|
||||||
|
TickDriver m_tickDriver;
|
||||||
|
QElapsedTimer m_frameTimer;
|
||||||
|
qint64 m_wallMs;
|
||||||
|
double m_gameSpeedMultiplier;
|
||||||
|
double m_prevNonZeroSpeed;
|
||||||
|
float m_scrollXTiles;
|
||||||
|
|
||||||
|
QTimer* m_renderTimer;
|
||||||
|
|
||||||
|
std::vector<ActiveBeam> m_activeBeams;
|
||||||
|
std::vector<ToastEntry> m_toasts;
|
||||||
|
|
||||||
|
std::optional<BuildingType> m_builderType;
|
||||||
|
Rotation m_ghostRotation;
|
||||||
|
QPoint m_ghostTile;
|
||||||
|
bool m_ghostValid;
|
||||||
|
std::set<QPoint, QPointCompare> m_beltDragTiles;
|
||||||
|
bool m_dragging;
|
||||||
|
|
||||||
|
bool m_demolishMode;
|
||||||
|
EntityId m_demolishHoverId;
|
||||||
|
|
||||||
|
std::vector<EntityId> m_selectedIds;
|
||||||
|
bool m_boxSelecting;
|
||||||
|
QPoint m_boxStartTile;
|
||||||
|
QPoint m_boxCurrentTile;
|
||||||
|
|
||||||
|
bool m_scrollLeft;
|
||||||
|
bool m_scrollRight;
|
||||||
|
bool m_gameOverShown;
|
||||||
|
};
|
||||||
72
src/ui/HeaderBar.cpp
Normal file
72
src/ui/HeaderBar.cpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#include "HeaderBar.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSignalMapper>
|
||||||
|
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 4.0 };
|
||||||
|
const int HeaderBar::kSpeedCount = 5;
|
||||||
|
|
||||||
|
HeaderBar::HeaderBar(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||||
|
layout->setContentsMargins(8, 4, 8, 4);
|
||||||
|
layout->setSpacing(8);
|
||||||
|
|
||||||
|
m_timeLabel = new QLabel("00:00", this);
|
||||||
|
m_blocksLabel = new QLabel("Blocks: 0", this);
|
||||||
|
layout->addWidget(m_timeLabel);
|
||||||
|
layout->addWidget(m_blocksLabel);
|
||||||
|
layout->addStretch();
|
||||||
|
|
||||||
|
const char* labels[] = { "0x", "0.5x", "1x", "2x", "4x" };
|
||||||
|
QSignalMapper* mapper = new QSignalMapper(this);
|
||||||
|
for (int i = 0; i < kSpeedCount; ++i)
|
||||||
|
{
|
||||||
|
QPushButton* btn = new QPushButton(labels[i], this);
|
||||||
|
btn->setCheckable(true);
|
||||||
|
btn->setChecked(i == 2);
|
||||||
|
layout->addWidget(btn);
|
||||||
|
m_speedButtons.push_back(btn);
|
||||||
|
mapper->setMapping(btn, i);
|
||||||
|
connect(btn, SIGNAL(clicked()), mapper, SLOT(map()));
|
||||||
|
}
|
||||||
|
connect(mapper, SIGNAL(mapped(int)), this, SLOT(onSpeedButton(int)));
|
||||||
|
|
||||||
|
setFixedHeight(sizeHint().height());
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeaderBar::onStateUpdated(Tick tick, int buildingBlocks, double gameSpeed)
|
||||||
|
{
|
||||||
|
const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
|
||||||
|
const int minutes = totalSeconds / 60;
|
||||||
|
const int seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
m_timeLabel->setText(
|
||||||
|
QString("%1:%2")
|
||||||
|
.arg(minutes, 2, 10, QChar('0'))
|
||||||
|
.arg(seconds, 2, 10, QChar('0')));
|
||||||
|
|
||||||
|
m_blocksLabel->setText(QString("Blocks: %1").arg(buildingBlocks));
|
||||||
|
|
||||||
|
for (int i = 0; i < kSpeedCount; ++i)
|
||||||
|
{
|
||||||
|
const bool active = (std::abs(kSpeeds[i] - gameSpeed) < 0.001);
|
||||||
|
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeaderBar::onSpeedButton(int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < kSpeedCount)
|
||||||
|
{
|
||||||
|
emit speedChanged(kSpeeds[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/ui/HeaderBar.h
Normal file
35
src/ui/HeaderBar.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
|
class HeaderBar : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit HeaderBar(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void onStateUpdated(Tick tick, int buildingBlocks, double gameSpeed);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void speedChanged(double multiplier);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onSpeedButton(int index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_timeLabel;
|
||||||
|
QLabel* m_blocksLabel;
|
||||||
|
std::vector<QPushButton*> m_speedButtons;
|
||||||
|
|
||||||
|
static const double kSpeeds[];
|
||||||
|
static const int kSpeedCount;
|
||||||
|
};
|
||||||
109
src/ui/MainWindow.cpp
Normal file
109
src/ui/MainWindow.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#include "MainWindow.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QResizeEvent>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BuildButtonGrid.h"
|
||||||
|
#include "GameWorldView.h"
|
||||||
|
#include "HeaderBar.h"
|
||||||
|
#include "SelectedBuildingPanel.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
MainWindow::MainWindow(Simulation* sim, const GameConfig* config,
|
||||||
|
const VisualsConfig* visuals, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_sim(sim)
|
||||||
|
{
|
||||||
|
setWindowTitle("Dota Factory");
|
||||||
|
resize(1280, 768);
|
||||||
|
|
||||||
|
m_headerBar = new HeaderBar(this);
|
||||||
|
|
||||||
|
m_gameWorldView = new GameWorldView(sim, config, 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, config, m_bottomPanel);
|
||||||
|
m_buildButtonGrid = new BuildButtonGrid(config, m_bottomPanel);
|
||||||
|
|
||||||
|
bottomLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||||
|
bottomLayout->addWidget(m_buildButtonGrid, 1);
|
||||||
|
|
||||||
|
// Signals: game world → other panels
|
||||||
|
connect(m_gameWorldView, SIGNAL(selectionChanged(std::vector<EntityId>)),
|
||||||
|
m_selectedBuildingPanel, SLOT(onSelectionChanged(std::vector<EntityId>)));
|
||||||
|
|
||||||
|
connect(m_gameWorldView, SIGNAL(stateUpdated(Tick, int, double)),
|
||||||
|
m_headerBar, SLOT(onStateUpdated(Tick, int, double)));
|
||||||
|
|
||||||
|
connect(m_gameWorldView, SIGNAL(stateUpdated(Tick, int, double)),
|
||||||
|
this, SLOT(onStateUpdated(Tick, int, double))); // for affordability
|
||||||
|
|
||||||
|
connect(m_gameWorldView, SIGNAL(gameOver()),
|
||||||
|
this, SLOT(onGameOver()));
|
||||||
|
|
||||||
|
// Signals: build grid → game world
|
||||||
|
connect(m_buildButtonGrid, SIGNAL(buildingTypeSelected(BuildingType)),
|
||||||
|
m_gameWorldView, SLOT(enterBuilderMode(BuildingType)));
|
||||||
|
|
||||||
|
connect(m_buildButtonGrid, SIGNAL(builderModeExited()),
|
||||||
|
m_gameWorldView, SLOT(exitBuilderMode()));
|
||||||
|
|
||||||
|
connect(m_gameWorldView, SIGNAL(builderModeExited()),
|
||||||
|
m_buildButtonGrid, SLOT(clearActiveButton()));
|
||||||
|
|
||||||
|
// Signals: header bar → game world
|
||||||
|
connect(m_headerBar, SIGNAL(speedChanged(double)),
|
||||||
|
m_gameWorldView, SLOT(setGameSpeed(double)));
|
||||||
|
|
||||||
|
m_gameWorldView->setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::resizeEvent(QResizeEvent* event)
|
||||||
|
{
|
||||||
|
QWidget::resizeEvent(event);
|
||||||
|
layoutPanels();
|
||||||
|
}
|
||||||
|
|
||||||
|
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::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')));
|
||||||
|
box.addButton("Quit", QMessageBox::RejectRole);
|
||||||
|
box.exec();
|
||||||
|
close();
|
||||||
|
}
|
||||||
40
src/ui/MainWindow.h
Normal file
40
src/ui/MainWindow.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
|
class Simulation;
|
||||||
|
class GameWorldView;
|
||||||
|
class HeaderBar;
|
||||||
|
class SelectedBuildingPanel;
|
||||||
|
class BuildButtonGrid;
|
||||||
|
class QResizeEvent;
|
||||||
|
|
||||||
|
class MainWindow : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
MainWindow(Simulation* sim, const GameConfig* config,
|
||||||
|
const VisualsConfig* visuals, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onGameOver();
|
||||||
|
void onStateUpdated(Tick tick, int blocks, double speed);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void layoutPanels();
|
||||||
|
|
||||||
|
Simulation* m_sim;
|
||||||
|
GameWorldView* m_gameWorldView;
|
||||||
|
HeaderBar* m_headerBar;
|
||||||
|
SelectedBuildingPanel* m_selectedBuildingPanel;
|
||||||
|
BuildButtonGrid* m_buildButtonGrid;
|
||||||
|
QWidget* m_bottomPanel;
|
||||||
|
};
|
||||||
287
src/ui/SelectedBuildingPanel.cpp
Normal file
287
src/ui/SelectedBuildingPanel.cpp
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
#include "SelectedBuildingPanel.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
QString buildingTypeName(BuildingType type)
|
||||||
|
{
|
||||||
|
const std::string id = buildingTypeId(type);
|
||||||
|
QString result;
|
||||||
|
bool nextUpper = true;
|
||||||
|
for (char c : id)
|
||||||
|
{
|
||||||
|
if (c == '_')
|
||||||
|
{
|
||||||
|
result += ' ';
|
||||||
|
nextUpper = true;
|
||||||
|
}
|
||||||
|
else if (nextUpper)
|
||||||
|
{
|
||||||
|
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
nextUpper = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isProductionBuilding(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Miner
|
||||||
|
|| type == BuildingType::Smelter
|
||||||
|
|| type == BuildingType::Assembler
|
||||||
|
|| type == BuildingType::ReprocessingPlant
|
||||||
|
|| type == BuildingType::Shipyard;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isBeltLike(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Belt || type == BuildingType::Splitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
||||||
|
const GameConfig* config,
|
||||||
|
QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_sim(sim)
|
||||||
|
, m_config(config)
|
||||||
|
, m_singleId(kInvalidEntityId)
|
||||||
|
{
|
||||||
|
m_layout = new QVBoxLayout(this);
|
||||||
|
m_layout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
m_layout->setSpacing(4);
|
||||||
|
m_layout->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
m_titleLabel = new QLabel(this);
|
||||||
|
m_recipeCombo = new QComboBox(this);
|
||||||
|
m_clearBeltBtn = new QPushButton("Clear Items", this);
|
||||||
|
m_buffersLabel = new QLabel(this);
|
||||||
|
m_buffersLabel->setWordWrap(true);
|
||||||
|
|
||||||
|
m_layout->addWidget(m_titleLabel);
|
||||||
|
m_layout->addWidget(m_recipeCombo);
|
||||||
|
m_layout->addWidget(m_clearBeltBtn);
|
||||||
|
m_layout->addWidget(m_buffersLabel);
|
||||||
|
|
||||||
|
connect(m_recipeCombo, SIGNAL(currentIndexChanged(int)),
|
||||||
|
this, SLOT(onRecipeChanged(int)));
|
||||||
|
connect(m_clearBeltBtn, SIGNAL(clicked()),
|
||||||
|
this, SLOT(onClearBelt()));
|
||||||
|
|
||||||
|
buildEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::onSelectionChanged(const std::vector<EntityId>& ids)
|
||||||
|
{
|
||||||
|
m_selection = ids;
|
||||||
|
rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::rebuild()
|
||||||
|
{
|
||||||
|
if (m_selection.empty())
|
||||||
|
{
|
||||||
|
buildEmpty();
|
||||||
|
}
|
||||||
|
else if (m_selection.size() == 1)
|
||||||
|
{
|
||||||
|
buildSingle(m_selection[0]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buildMulti(m_selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::buildEmpty()
|
||||||
|
{
|
||||||
|
m_singleId = kInvalidEntityId;
|
||||||
|
m_titleLabel->hide();
|
||||||
|
m_recipeCombo->hide();
|
||||||
|
m_clearBeltBtn->hide();
|
||||||
|
m_buffersLabel->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::buildSingle(EntityId id)
|
||||||
|
{
|
||||||
|
m_singleId = id;
|
||||||
|
|
||||||
|
const Building* b = m_sim->buildings().findBuilding(id);
|
||||||
|
if (!b)
|
||||||
|
{
|
||||||
|
buildEmpty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_titleLabel->setText(buildingTypeName(b->type));
|
||||||
|
m_titleLabel->show();
|
||||||
|
m_buffersLabel->show();
|
||||||
|
|
||||||
|
if (isProductionBuilding(b->type))
|
||||||
|
{
|
||||||
|
m_recipeCombo->blockSignals(true);
|
||||||
|
m_recipeCombo->clear();
|
||||||
|
|
||||||
|
m_recipeCombo->addItem("(none)", QString());
|
||||||
|
|
||||||
|
if (b->type == BuildingType::Shipyard)
|
||||||
|
{
|
||||||
|
for (const ShipDef& def : m_config->ships.ships)
|
||||||
|
{
|
||||||
|
if (m_sim->isBlueprintUnlocked(def.id))
|
||||||
|
{
|
||||||
|
m_recipeCombo->addItem(
|
||||||
|
QString::fromStdString(def.id),
|
||||||
|
QString::fromStdString(def.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (const RecipeDef& recipe : m_config->recipes.recipes)
|
||||||
|
{
|
||||||
|
if (recipe.building == b->type)
|
||||||
|
{
|
||||||
|
m_recipeCombo->addItem(
|
||||||
|
QString::fromStdString(recipe.id),
|
||||||
|
QString::fromStdString(recipe.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int currentIdx = m_recipeCombo->findData(QString::fromStdString(b->recipeId));
|
||||||
|
m_recipeCombo->setCurrentIndex(currentIdx >= 0 ? currentIdx : 0);
|
||||||
|
m_recipeCombo->blockSignals(false);
|
||||||
|
m_recipeCombo->show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_recipeCombo->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBeltLike(b->type))
|
||||||
|
{
|
||||||
|
m_clearBeltBtn->show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_clearBeltBtn->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString bufText;
|
||||||
|
if (!b->inputBuffer.counts.empty())
|
||||||
|
{
|
||||||
|
bufText += "Input: ";
|
||||||
|
for (const std::pair<const ItemType, int>& entry : b->inputBuffer.counts)
|
||||||
|
{
|
||||||
|
const std::map<ItemType, int>::const_iterator cap =
|
||||||
|
b->inputBuffer.caps.find(entry.first);
|
||||||
|
const int capVal = (cap != b->inputBuffer.caps.end()) ? cap->second : 0;
|
||||||
|
bufText += QString::fromStdString(entry.first.id)
|
||||||
|
+ ": " + QString::number(entry.second)
|
||||||
|
+ "/" + QString::number(capVal) + " ";
|
||||||
|
}
|
||||||
|
bufText += "\n";
|
||||||
|
}
|
||||||
|
if (!b->outputBuffer.items.empty())
|
||||||
|
{
|
||||||
|
std::map<std::string, int> outCounts;
|
||||||
|
for (const Item& item : b->outputBuffer.items)
|
||||||
|
{
|
||||||
|
outCounts[item.type.id]++;
|
||||||
|
}
|
||||||
|
bufText += "Output(" + QString::number(static_cast<int>(b->outputBuffer.items.size()))
|
||||||
|
+ "/" + QString::number(b->outputBuffer.capacity) + "): ";
|
||||||
|
for (const std::pair<const std::string, int>& entry : outCounts)
|
||||||
|
{
|
||||||
|
bufText += QString::fromStdString(entry.first)
|
||||||
|
+ ":" + QString::number(entry.second) + " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_buffersLabel->setText(bufText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::buildMulti(const std::vector<EntityId>& ids)
|
||||||
|
{
|
||||||
|
m_singleId = kInvalidEntityId;
|
||||||
|
m_recipeCombo->hide();
|
||||||
|
m_clearBeltBtn->hide();
|
||||||
|
m_buffersLabel->hide();
|
||||||
|
|
||||||
|
std::map<BuildingType, int> counts;
|
||||||
|
for (EntityId id : ids)
|
||||||
|
{
|
||||||
|
const Building* b = m_sim->buildings().findBuilding(id);
|
||||||
|
if (b)
|
||||||
|
{
|
||||||
|
counts[b->type]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasBelt = false;
|
||||||
|
QString text;
|
||||||
|
for (const std::pair<const BuildingType, int>& entry : counts)
|
||||||
|
{
|
||||||
|
text += buildingTypeName(entry.first) + ": "
|
||||||
|
+ QString::number(entry.second) + "\n";
|
||||||
|
if (isBeltLike(entry.first))
|
||||||
|
{
|
||||||
|
hasBelt = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_titleLabel->setText(text.trimmed());
|
||||||
|
m_titleLabel->show();
|
||||||
|
|
||||||
|
if (hasBelt)
|
||||||
|
{
|
||||||
|
m_clearBeltBtn->show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::onRecipeChanged(int comboIndex)
|
||||||
|
{
|
||||||
|
if (m_singleId == kInvalidEntityId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString recipeId = m_recipeCombo->itemData(comboIndex).toString();
|
||||||
|
m_sim->buildings().setRecipe(m_singleId, recipeId.toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::onClearBelt()
|
||||||
|
{
|
||||||
|
std::vector<QPoint> tiles;
|
||||||
|
for (EntityId id : m_selection)
|
||||||
|
{
|
||||||
|
const Building* b = m_sim->buildings().findBuilding(id);
|
||||||
|
if (b && isBeltLike(b->type))
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : b->bodyCells)
|
||||||
|
{
|
||||||
|
tiles.push_back(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tiles.empty())
|
||||||
|
{
|
||||||
|
m_sim->belts().clearTiles(tiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/ui/SelectedBuildingPanel.h
Normal file
51
src/ui/SelectedBuildingPanel.h
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "EntityId.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
|
||||||
|
class Simulation;
|
||||||
|
class QLabel;
|
||||||
|
class QComboBox;
|
||||||
|
class QPushButton;
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
class SelectedBuildingPanel : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
SelectedBuildingPanel(Simulation* sim, const GameConfig* config,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void onSelectionChanged(const std::vector<EntityId>& ids);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onRecipeChanged(int comboIndex);
|
||||||
|
void onClearBelt();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void rebuild();
|
||||||
|
void clearContent();
|
||||||
|
void buildEmpty();
|
||||||
|
void buildSingle(EntityId id);
|
||||||
|
void buildMulti(const std::vector<EntityId>& ids);
|
||||||
|
|
||||||
|
Simulation* m_sim;
|
||||||
|
const GameConfig* m_config;
|
||||||
|
std::vector<EntityId> m_selection;
|
||||||
|
|
||||||
|
QVBoxLayout* m_layout;
|
||||||
|
QLabel* m_titleLabel;
|
||||||
|
QComboBox* m_recipeCombo;
|
||||||
|
QPushButton* m_clearBeltBtn;
|
||||||
|
QLabel* m_buffersLabel;
|
||||||
|
|
||||||
|
EntityId m_singleId;
|
||||||
|
std::string m_currentRecipeId;
|
||||||
|
};
|
||||||
78
src/ui/VisualsConfig.h
Normal file
78
src/ui/VisualsConfig.h
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
|
||||||
|
struct TileVisuals
|
||||||
|
{
|
||||||
|
QColor fill;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BuildingVisuals
|
||||||
|
{
|
||||||
|
QColor fill;
|
||||||
|
QColor outline;
|
||||||
|
QString glyph;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ItemVisuals
|
||||||
|
{
|
||||||
|
QColor fill;
|
||||||
|
QColor outline;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShipVisuals
|
||||||
|
{
|
||||||
|
QColor fill;
|
||||||
|
QColor outline;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BeamVisuals
|
||||||
|
{
|
||||||
|
QColor color;
|
||||||
|
int widthPx;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OverlayVisuals
|
||||||
|
{
|
||||||
|
QColor ghostValid;
|
||||||
|
QColor ghostInvalid;
|
||||||
|
QColor demolishTint;
|
||||||
|
QColor selectionRect;
|
||||||
|
QColor tileHighlight;
|
||||||
|
QColor selectedOutline;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToastVisuals
|
||||||
|
{
|
||||||
|
QColor bg;
|
||||||
|
QColor fg;
|
||||||
|
int fontSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ShipRole
|
||||||
|
{
|
||||||
|
PlayerCombat,
|
||||||
|
Salvage,
|
||||||
|
Repair,
|
||||||
|
Enemy,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VisualsConfig
|
||||||
|
{
|
||||||
|
TileVisuals asteroid;
|
||||||
|
TileVisuals space;
|
||||||
|
|
||||||
|
std::map<BuildingType, BuildingVisuals> buildings;
|
||||||
|
std::map<std::string, ItemVisuals> items;
|
||||||
|
std::map<ShipRole, ShipVisuals> ships;
|
||||||
|
|
||||||
|
BeamVisuals beams;
|
||||||
|
OverlayVisuals overlays;
|
||||||
|
ToastVisuals toast;
|
||||||
|
};
|
||||||
231
src/ui/VisualsLoader.cpp
Normal file
231
src/ui/VisualsLoader.cpp
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
#include "VisualsLoader.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
std::runtime_error makeError(const std::string& section, const std::string& why)
|
||||||
|
{
|
||||||
|
return std::runtime_error("visuals.toml: [" + section + "] " + why);
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor parseColor(const std::string& s, const std::string& ctx)
|
||||||
|
{
|
||||||
|
if (s.empty() || s[0] != '#')
|
||||||
|
{
|
||||||
|
throw std::runtime_error("visuals.toml: invalid color '" + s + "' in " + ctx);
|
||||||
|
}
|
||||||
|
if (s.size() == 9)
|
||||||
|
{
|
||||||
|
bool ok1 = true, ok2 = true, ok3 = true, ok4 = true;
|
||||||
|
int r = QString::fromStdString(s.substr(1, 2)).toInt(&ok1, 16);
|
||||||
|
int g = QString::fromStdString(s.substr(3, 2)).toInt(&ok2, 16);
|
||||||
|
int b = QString::fromStdString(s.substr(5, 2)).toInt(&ok3, 16);
|
||||||
|
int a = QString::fromStdString(s.substr(7, 2)).toInt(&ok4, 16);
|
||||||
|
if (!ok1 || !ok2 || !ok3 || !ok4)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("visuals.toml: malformed color '" + s + "' in " + ctx);
|
||||||
|
}
|
||||||
|
return QColor(r, g, b, a);
|
||||||
|
}
|
||||||
|
QColor c(QString::fromStdString(s));
|
||||||
|
if (!c.isValid())
|
||||||
|
{
|
||||||
|
throw std::runtime_error("visuals.toml: invalid color '" + s + "' in " + ctx);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string requireString(toml::table& tbl, const std::string& key,
|
||||||
|
const std::string& ctx)
|
||||||
|
{
|
||||||
|
const std::optional<std::string> v = tbl[key].value<std::string>();
|
||||||
|
if (!v)
|
||||||
|
{
|
||||||
|
throw makeError(ctx, "missing or invalid string '" + key + "'");
|
||||||
|
}
|
||||||
|
return *v;
|
||||||
|
}
|
||||||
|
|
||||||
|
int requireInt(toml::table& tbl, const std::string& key, const std::string& ctx)
|
||||||
|
{
|
||||||
|
const std::optional<int64_t> v = tbl[key].value<int64_t>();
|
||||||
|
if (!v)
|
||||||
|
{
|
||||||
|
throw makeError(ctx, "missing or invalid integer '" + key + "'");
|
||||||
|
}
|
||||||
|
return static_cast<int>(*v);
|
||||||
|
}
|
||||||
|
|
||||||
|
toml::table& requireSubtable(toml::table& tbl, const std::string& key,
|
||||||
|
const std::string& ctx)
|
||||||
|
{
|
||||||
|
toml::table* sub = tbl[key].as_table();
|
||||||
|
if (sub == nullptr)
|
||||||
|
{
|
||||||
|
throw makeError(ctx, "missing section '" + key + "'");
|
||||||
|
}
|
||||||
|
return *sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
TileVisuals parseTile(toml::table& tbl, const std::string& ctx)
|
||||||
|
{
|
||||||
|
TileVisuals v;
|
||||||
|
v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill");
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildingVisuals parseBuilding(toml::table& tbl, const std::string& ctx)
|
||||||
|
{
|
||||||
|
BuildingVisuals v;
|
||||||
|
v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill");
|
||||||
|
v.outline = parseColor(requireString(tbl, "outline", ctx), ctx + ".outline");
|
||||||
|
v.glyph = QString::fromStdString(requireString(tbl, "glyph", ctx));
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemVisuals parseItem(toml::table& tbl, const std::string& ctx)
|
||||||
|
{
|
||||||
|
ItemVisuals v;
|
||||||
|
v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill");
|
||||||
|
v.outline = parseColor(requireString(tbl, "outline", ctx), ctx + ".outline");
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShipVisuals parseShip(toml::table& tbl, const std::string& ctx)
|
||||||
|
{
|
||||||
|
ShipVisuals v;
|
||||||
|
v.fill = parseColor(requireString(tbl, "fill", ctx), ctx + ".fill");
|
||||||
|
v.outline = parseColor(requireString(tbl, "outline", ctx), ctx + ".outline");
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BuildingEntry
|
||||||
|
{
|
||||||
|
const char* key;
|
||||||
|
BuildingType type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BuildingEntry kBuildingEntries[] = {
|
||||||
|
{ "hq", BuildingType::Hq },
|
||||||
|
{ "miner", BuildingType::Miner },
|
||||||
|
{ "smelter", BuildingType::Smelter },
|
||||||
|
{ "assembler", BuildingType::Assembler },
|
||||||
|
{ "reprocessing_plant",BuildingType::ReprocessingPlant },
|
||||||
|
{ "shipyard", BuildingType::Shipyard },
|
||||||
|
{ "salvage_bay", BuildingType::SalvageBay },
|
||||||
|
{ "belt", BuildingType::Belt },
|
||||||
|
{ "splitter", BuildingType::Splitter },
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
VisualsConfig VisualsLoader::load(const std::string& path)
|
||||||
|
{
|
||||||
|
toml::table tbl;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tbl = toml::parse_file(path);
|
||||||
|
}
|
||||||
|
catch (const toml::parse_error& e)
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "visuals.toml: parse error: " << e.description()
|
||||||
|
<< " at " << e.source().begin;
|
||||||
|
throw std::runtime_error(oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
VisualsConfig cfg;
|
||||||
|
|
||||||
|
// Tiles
|
||||||
|
{
|
||||||
|
toml::table& tiles = requireSubtable(tbl, "tiles", "root");
|
||||||
|
cfg.asteroid = parseTile(requireSubtable(tiles, "asteroid", "tiles"), "tiles.asteroid");
|
||||||
|
cfg.space = parseTile(requireSubtable(tiles, "space", "tiles"), "tiles.space");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buildings ([buildings.*] sections)
|
||||||
|
{
|
||||||
|
toml::table& bldgs = requireSubtable(tbl, "buildings", "root");
|
||||||
|
for (const BuildingEntry& entry : kBuildingEntries)
|
||||||
|
{
|
||||||
|
std::string ctx = std::string("buildings.") + entry.key;
|
||||||
|
cfg.buildings[entry.type] = parseBuilding(
|
||||||
|
requireSubtable(bldgs, entry.key, "buildings"), ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stations ([stations.*] → mapped as PlayerDefenceStation / EnemyDefenceStation)
|
||||||
|
{
|
||||||
|
toml::table& stns = requireSubtable(tbl, "stations", "root");
|
||||||
|
cfg.buildings[BuildingType::PlayerDefenceStation] = parseBuilding(
|
||||||
|
requireSubtable(stns, "player", "stations"), "stations.player");
|
||||||
|
cfg.buildings[BuildingType::EnemyDefenceStation] = parseBuilding(
|
||||||
|
requireSubtable(stns, "enemy", "stations"), "stations.enemy");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items (iterate all keys in [items])
|
||||||
|
{
|
||||||
|
toml::table& items = requireSubtable(tbl, "items", "root");
|
||||||
|
for (toml::table::iterator it = items.begin(); it != items.end(); ++it)
|
||||||
|
{
|
||||||
|
std::string itemId = std::string(it->first.str());
|
||||||
|
toml::table* sub = it->second.as_table();
|
||||||
|
if (sub == nullptr)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("visuals.toml: items." + itemId + " is not a table");
|
||||||
|
}
|
||||||
|
cfg.items[itemId] = parseItem(*sub, "items." + itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ships
|
||||||
|
{
|
||||||
|
toml::table& ships = requireSubtable(tbl, "ships", "root");
|
||||||
|
cfg.ships[ShipRole::PlayerCombat] = parseShip(
|
||||||
|
requireSubtable(ships, "player_combat", "ships"), "ships.player_combat");
|
||||||
|
cfg.ships[ShipRole::Salvage] = parseShip(
|
||||||
|
requireSubtable(ships, "salvage", "ships"), "ships.salvage");
|
||||||
|
cfg.ships[ShipRole::Repair] = parseShip(
|
||||||
|
requireSubtable(ships, "repair", "ships"), "ships.repair");
|
||||||
|
cfg.ships[ShipRole::Enemy] = parseShip(
|
||||||
|
requireSubtable(ships, "enemy", "ships"), "ships.enemy");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beams
|
||||||
|
{
|
||||||
|
toml::table& beams = requireSubtable(tbl, "beams", "root");
|
||||||
|
cfg.beams.color = parseColor(requireString(beams, "color", "beams"), "beams.color");
|
||||||
|
cfg.beams.widthPx = requireInt(beams, "width_px", "beams");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlays
|
||||||
|
{
|
||||||
|
toml::table& ov = requireSubtable(tbl, "overlays", "root");
|
||||||
|
cfg.overlays.ghostValid = parseColor(requireString(ov, "ghost_valid", "overlays"), "overlays.ghost_valid");
|
||||||
|
cfg.overlays.ghostInvalid = parseColor(requireString(ov, "ghost_invalid", "overlays"), "overlays.ghost_invalid");
|
||||||
|
cfg.overlays.demolishTint = parseColor(requireString(ov, "demolish_tint", "overlays"), "overlays.demolish_tint");
|
||||||
|
cfg.overlays.selectionRect = parseColor(requireString(ov, "selection_rect", "overlays"), "overlays.selection_rect");
|
||||||
|
cfg.overlays.tileHighlight = parseColor(requireString(ov, "tile_highlight", "overlays"), "overlays.tile_highlight");
|
||||||
|
cfg.overlays.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toast
|
||||||
|
{
|
||||||
|
toml::table& t = requireSubtable(tbl, "toast", "root");
|
||||||
|
cfg.toast.bg = parseColor(requireString(t, "bg", "toast"), "toast.bg");
|
||||||
|
cfg.toast.fg = parseColor(requireString(t, "fg", "toast"), "toast.fg");
|
||||||
|
cfg.toast.fontSize = requireInt(t, "font_size", "toast");
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
11
src/ui/VisualsLoader.h
Normal file
11
src/ui/VisualsLoader.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
|
class VisualsLoader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static VisualsConfig load(const std::string& path);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user