This commit is contained in:
bart
2026-11-08 16:15:49 +01:00
parent 5af4581182
commit 1140dd562b
34 changed files with 940 additions and 44 deletions
+10
View File
@@ -4,6 +4,16 @@ This document serves as the primary technical specification, architecture refere
---
## 0. Mandatory Agent Rule: All Values Must Be Configurable via YAML
> [!IMPORTANT]
> **Strict Agent Directive:** All gameplay values, numeric parameters, costs, multipliers, initial statistics, thresholds, ratios, yields, and game limits mentioned by the user MUST be implemented as configurable parameters loaded from YAML files (in the `config/` directory) rather than being hardcoded in C++ code.
> - Whenever a new value, parameter, or mechanic is introduced, create or update a corresponding YAML configuration file and load it at startup.
> - Default values in C++ must only serve as fallbacks if the YAML key is absent.
> - Keep configurations modular (e.g. `config/city.yaml`, `config/terrain_yields.yaml`, `config/game.yaml`, `config/units/*.yaml`).
---
## 1. Project Vision & Core Concept
**Goal:** Transform this 3D hexagonal terrain engine into a seamless **Real-Time Strategy Civilization** game.
+28 -5
View File
@@ -64,6 +64,8 @@ SOURCES = main.cpp \
unitconfig.cpp \
pathfinder.cpp \
unit.cpp \
city.cpp \
terrainyieldmanager.cpp \
gamemanager.cpp \
unitrenderer.cpp qrc_shaders.cpp \
moc_mainwindow.cpp
@@ -79,6 +81,8 @@ OBJECTS = main.o \
unitconfig.o \
pathfinder.o \
unit.o \
city.o \
terrainyieldmanager.o \
gamemanager.o \
unitrenderer.o \
qrc_shaders.o \
@@ -188,6 +192,7 @@ DIST = shaders/simple.vert \
pathfinder.h \
unit.h \
city.h \
terrainyieldmanager.h \
gamemanager.h \
unitrenderer.h main.cpp \
mainwindow.cpp \
@@ -201,6 +206,8 @@ DIST = shaders/simple.vert \
unitconfig.cpp \
pathfinder.cpp \
unit.cpp \
city.cpp \
terrainyieldmanager.cpp \
gamemanager.cpp \
unitrenderer.cpp
QMAKE_TARGET = tiletest
@@ -409,8 +416,8 @@ distdir: FORCE
$(COPY_FILE) --parents $(DIST) $(DISTDIR)/
$(COPY_FILE) --parents shaders.qrc $(DISTDIR)/
$(COPY_FILE) --parents /usr/lib/x86_64-linux-gnu/qt5/mkspecs/features/data/dummy.cpp $(DISTDIR)/
$(COPY_FILE) --parents mainwindow.h qscenerenderer.h vertex.h tile.h terrain.h tilevertexinfo.h matrix2d.h face.h player.h yamlparser.h unitconfig.h pathfinder.h unit.h city.h gamemanager.h unitrenderer.h $(DISTDIR)/
$(COPY_FILE) --parents main.cpp mainwindow.cpp qscenerenderer.cpp vertex.cpp tile.cpp tilevertexinfo.cpp face.cpp player.cpp yamlparser.cpp unitconfig.cpp pathfinder.cpp unit.cpp gamemanager.cpp unitrenderer.cpp $(DISTDIR)/
$(COPY_FILE) --parents mainwindow.h qscenerenderer.h vertex.h tile.h terrain.h tilevertexinfo.h matrix2d.h face.h player.h yamlparser.h unitconfig.h pathfinder.h unit.h city.h terrainyieldmanager.h gamemanager.h unitrenderer.h $(DISTDIR)/
$(COPY_FILE) --parents main.cpp mainwindow.cpp qscenerenderer.cpp vertex.cpp tile.cpp tilevertexinfo.cpp face.cpp player.cpp yamlparser.cpp unitconfig.cpp pathfinder.cpp unit.cpp city.cpp terrainyieldmanager.cpp gamemanager.cpp unitrenderer.cpp $(DISTDIR)/
$(COPY_FILE) --parents mainwindow.ui $(DISTDIR)/
@@ -479,7 +486,8 @@ ui_mainwindow.h: mainwindow.ui \
unitconfig.h \
player.h \
city.h \
gamemanager.h
gamemanager.h \
terrainyieldmanager.h
/usr/lib/qt5/bin/uic mainwindow.ui -o ui_mainwindow.h
compiler_yacc_decl_make_all:
@@ -509,7 +517,8 @@ mainwindow.o: mainwindow.cpp mainwindow.h \
unitconfig.h \
player.h \
city.h \
gamemanager.h
gamemanager.h \
terrainyieldmanager.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o mainwindow.o mainwindow.cpp
qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
@@ -525,6 +534,7 @@ qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
player.h \
city.h \
gamemanager.h \
terrainyieldmanager.h \
pathfinder.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o qscenerenderer.o qscenerenderer.cpp
@@ -545,7 +555,8 @@ tile.o: tile.cpp tile.h \
unitconfig.h \
player.h \
city.h \
gamemanager.h
gamemanager.h \
terrainyieldmanager.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o tile.o tile.cpp
tilevertexinfo.o: tilevertexinfo.cpp tilevertexinfo.h \
@@ -580,6 +591,17 @@ unit.o: unit.cpp unit.h \
pathfinder.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o unit.o unit.cpp
city.o: city.cpp city.h \
terrain.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o city.o city.cpp
terrainyieldmanager.o: terrainyieldmanager.cpp terrainyieldmanager.h \
terrain.h \
matrix2d.h \
yamlparser.h \
pathfinder.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o terrainyieldmanager.o terrainyieldmanager.cpp
gamemanager.o: gamemanager.cpp gamemanager.h \
player.h \
unitconfig.h \
@@ -587,6 +609,7 @@ gamemanager.o: gamemanager.cpp gamemanager.h \
terrain.h \
matrix2d.h \
city.h \
terrainyieldmanager.h \
pathfinder.h \
yamlparser.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o gamemanager.o gamemanager.cpp
+21
View File
@@ -0,0 +1,21 @@
#include "city.h"
City::City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos,
int initInhabitants, int initInhabitantsPerPop, int initRadius)
: id(cId), playerId(pId), name(cName), tileX(tx), tileY(ty), worldPos(pos),
inhabitants(initInhabitants), inhabitantsPerPop(initInhabitantsPerPop)
{
// Claim center tile
territory.emplace_back(tx, ty);
// Claim surrounding 6 tiles (radius 1)
if (initRadius >= 1) {
for (int e = 0; e < 6; ++e) {
int nx, ny;
getHexNeighbor(tx, ty, e, nx, ny);
if (nx >= 0 && nx < MAP_WIDTH && ny >= 0 && ny < MAP_HEIGHT) {
territory.emplace_back(nx, ny);
}
}
}
}
+80 -4
View File
@@ -3,8 +3,23 @@
#include <QString>
#include <QVector3D>
#include <vector>
#include <utility>
#include <map>
#include <cmath>
#include "terrain.h"
struct City {
struct CityYieldSummary {
int food = 0;
int labor = 0;
int science = 0;
int culture = 0;
int faith = 0;
int gold = 0;
};
class City {
public:
int id;
int playerId;
QString name;
@@ -12,9 +27,70 @@ struct City {
int tileY;
QVector3D worldPos;
City() : id(0), playerId(0), name(""), tileX(0), tileY(0), worldPos(0, 0, 0) {}
City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos)
: id(cId), playerId(pId), name(cName), tileX(tx), tileY(ty), worldPos(pos) {}
int inhabitants;
int inhabitantsPerPop;
std::vector<std::pair<int, int>> territory; // Owned hex coordinates
std::map<std::pair<int, int>, QString> workedTiles; // (x, y) -> resource name
City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos,
int initInhabitants = 1000, int initInhabitantsPerPop = 100000, int initRadius = 1);
int getTotalPop() const {
return (int)std::ceil((float)inhabitants / (float)std::max(1, inhabitantsPerPop));
}
int getAssignedPop() const {
return (int)workedTiles.size();
}
int getAvailablePop() const {
return std::max(0, getTotalPop() - getAssignedPop());
}
bool ownsTile(int tx, int ty) const {
for (const auto &p : territory) {
if (p.first == tx && p.second == ty) return true;
}
return false;
}
bool isTileWorked(int tx, int ty) const {
return workedTiles.find({tx, ty}) != workedTiles.end();
}
QString getWorkedResource(int tx, int ty) const {
auto it = workedTiles.find({tx, ty});
return it != workedTiles.end() ? it->second : "";
}
bool assignWorker(int tx, int ty, const QString &resource) {
if (!ownsTile(tx, ty)) return false;
if (isTileWorked(tx, ty)) {
workedTiles[{tx, ty}] = resource;
return true;
}
if (getAvailablePop() <= 0) return false;
workedTiles[{tx, ty}] = resource;
return true;
}
bool unassignWorker(int tx, int ty) {
auto it = workedTiles.find({tx, ty});
if (it != workedTiles.end()) {
workedTiles.erase(it);
return true;
}
return false;
}
void toggleWorker(int tx, int ty, const QString &defaultRes) {
if (isTileWorked(tx, ty)) {
unassignWorker(tx, ty);
} else {
assignWorker(tx, ty, defaultRes);
}
}
};
#endif // CITY_H
BIN
View File
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
# City configuration parameters
# All values are configurable
initial_inhabitants: 1000
inhabitants_per_pop: 100000
initial_border_radius: 1
max_workable_radius: 3
default_tile_slots: 1
+106
View File
@@ -0,0 +1,106 @@
# Terrain yield and workability configuration
# All yields, workability, and slot numbers are fully configurable
grasland:
name: "Grasland"
workable: true
default_slots: 1
primary_resource: "voedsel"
yields:
voedsel: 2000
arbeid: 100
kennis: 100
cultuur: 100
geloof: 100
goud: 100
bos:
name: "Bos"
workable: true
default_slots: 1
primary_resource: "arbeid"
yields:
voedsel: 500
arbeid: 2000
kennis: 100
cultuur: 100
geloof: 100
goud: 100
steen:
name: "Steen / Berg"
workable: true
default_slots: 1
primary_resource: "arbeid"
yields:
voedsel: 10
arbeid: 10
kennis: 100
cultuur: 100
geloof: 100
goud: 100
sahara:
name: "Woestijn"
workable: true
default_slots: 1
primary_resource: "goud"
yields:
voedsel: 50
arbeid: 50
kennis: 100
cultuur: 100
geloof: 100
goud: 500
tundra:
name: "Tundra"
workable: true
default_slots: 1
primary_resource: "voedsel"
yields:
voedsel: 400
arbeid: 400
kennis: 100
cultuur: 100
geloof: 100
goud: 100
red_canyon:
name: "Red Canyon"
workable: true
default_slots: 1
primary_resource: "arbeid"
yields:
voedsel: 100
arbeid: 1200
kennis: 100
cultuur: 100
geloof: 100
goud: 300
sneeuw:
name: "Sneeuw"
workable: false
default_slots: 0
primary_resource: "geen"
yields:
voedsel: 0
arbeid: 0
kennis: 0
cultuur: 0
geloof: 0
goud: 0
water:
name: "Water"
workable: false
default_slots: 0
primary_resource: "geen"
yields:
voedsel: 0
arbeid: 0
kennis: 0
cultuur: 0
geloof: 0
goud: 0
+102 -5
View File
@@ -1,6 +1,7 @@
#include "gamemanager.h"
#include "pathfinder.h"
#include "yamlparser.h"
#include "terrainyieldmanager.h"
#include <QDebug>
#include <cmath>
@@ -11,7 +12,7 @@ GameManager& GameManager::instance()
}
GameManager::GameManager()
: m_selectedUnitId(-1), m_actionMode(ActionMode::None),
: m_selectedUnitId(-1), m_activeCityViewId(-1), m_actionMode(ActionMode::None),
m_movementTickDuration(30.0f), m_tickTimer(0.0f),
m_simSpeedMultiplier(1.0f), m_nextUnitId(0), m_nextCityId(0),
m_initialized(false)
@@ -22,6 +23,9 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
{
if (m_initialized) return;
// Load terrain yields & city configuration
TerrainYieldManager::instance().loadConfigs(baseDir);
// Load game settings
QString gameConfigPath = baseDir + "/config/game.yaml";
YamlNode gameRoot = YamlParser::parseFile(gameConfigPath);
@@ -33,7 +37,24 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
QString unitsConfigDir = baseDir + "/config/units";
m_unitConfigManager.loadFromDirectory(unitsConfigDir);
// Spawn initial demo units near starting camera position (Cam: ~28, 16 km -> hex ~37, 18)
// Spawn an initial established city for Player 1 at (35, 17)
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
int cId = getNextCityId();
QVector2D w = Pathfinder::hexToWorld(35, 17);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
City initialCity(cId, 1, "Roma", 35, 17, QVector3D(w.x(), w.y(), gz),
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
// Assign initial 1 pop to work the city center tile
QString tType = TerrainYieldManager::instance().getTerrainType(35, 17, mapPoints, numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
QString defRes = yCfg ? yCfg->primaryResource : "voedsel";
initialCity.assignWorker(35, 17, defRes);
m_cities.push_back(initialCity);
// Spawn initial demo units near starting camera position
// Player 1 (Blue & Gold)
spawnUnit("settler", 1, 36, 18, mapPoints, numPoints05);
spawnUnit("warrior", 1, 37, 18, mapPoints, numPoints05);
@@ -44,7 +65,7 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
spawnUnit("slinger", 2, 41, 19, mapPoints, numPoints05);
m_initialized = true;
qDebug() << "GameManager initialized with" << m_units.size() << "units.";
qDebug() << "GameManager initialized with" << m_cities.size() << "cities and" << m_units.size() << "units.";
}
Unit* GameManager::spawnUnit(const QString &configId, int playerId, int tileX, int tileY,
@@ -96,7 +117,7 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
// Update floating texts
for (auto it = m_floatingTexts.begin(); it != m_floatingTexts.end(); ) {
it->life -= deltaTime;
it->worldPos.setZ(it->worldPos.z() + deltaTime * 0.4f); // Float upward
it->worldPos.setZ(it->worldPos.z() + deltaTime * 0.4f);
if (it->life <= 0.0f) {
it = m_floatingTexts.erase(it);
} else {
@@ -121,6 +142,7 @@ const Unit* GameManager::getSelectedUnit() const
void GameManager::selectUnit(int id)
{
m_selectedUnitId = id;
m_activeCityViewId = -1; // Close city view when unit is selected
m_actionMode = ActionMode::None;
}
@@ -148,6 +170,70 @@ Unit* GameManager::getUnitById(int id)
return nullptr;
}
City* GameManager::getCityAt(int tx, int ty)
{
for (auto &c : m_cities) {
if (c.tileX == tx && c.tileY == ty) return &c;
}
return nullptr;
}
City* GameManager::getCityById(int id)
{
for (auto &c : m_cities) {
if (c.id == id) return &c;
}
return nullptr;
}
City* GameManager::getActiveCityView()
{
return getCityById(m_activeCityViewId);
}
const City* GameManager::getActiveCityView() const
{
for (const auto &c : m_cities) {
if (c.id == m_activeCityViewId) return &c;
}
return nullptr;
}
void GameManager::enterCityView(int cityId)
{
m_activeCityViewId = cityId;
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
void GameManager::exitCityView()
{
m_activeCityViewId = -1;
}
CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
{
CityYieldSummary summary;
for (const auto &pair : city.workedTiles) {
int tx = pair.first.first;
int ty = pair.first.second;
QString assignedRes = pair.second;
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
if (!yCfg) continue;
int amount = yCfg->getYield(assignedRes);
if (assignedRes == "voedsel") summary.food += amount;
else if (assignedRes == "arbeid") summary.labor += amount;
else if (assignedRes == "kennis") summary.science += amount;
else if (assignedRes == "cultuur") summary.culture += amount;
else if (assignedRes == "geloof") summary.faith += amount;
else if (assignedRes == "goud") summary.gold += amount;
}
return summary;
}
void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
@@ -203,12 +289,23 @@ void GameManager::commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, in
}
}
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
int cId = getNextCityId();
QString cityName = QString("Stad %1").arg(cId);
QVector2D w = Pathfinder::hexToWorld(selected->tileX, selected->tileY);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
m_cities.emplace_back(cId, selected->playerId, cityName, selected->tileX, selected->tileY, QVector3D(w.x(), w.y(), gz));
City newCity(cId, selected->playerId, cityName, selected->tileX, selected->tileY,
QVector3D(w.x(), w.y(), gz),
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
// Assign initial worker to center tile
QString tType = TerrainYieldManager::instance().getTerrainType(selected->tileX, selected->tileY, mapPoints, numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
QString defRes = yCfg ? yCfg->primaryResource : "voedsel";
newCity.assignWorker(selected->tileX, selected->tileY, defRes);
m_cities.push_back(newCity);
addFloatingText(QString("%1 gesticht!").arg(cityName), QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f);
// Consuming settler
+16
View File
@@ -10,6 +10,7 @@
#include "city.h"
#include "matrix2d.h"
#include "terrain.h"
#include "terrainyieldmanager.h"
enum class ActionMode {
None,
@@ -38,6 +39,7 @@ public:
const std::vector<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
const std::vector<City>& getCities() const { return m_cities; }
std::vector<City>& getCitiesMutable() { return m_cities; }
const std::vector<FloatingText>& getFloatingTexts() const { return m_floatingTexts; }
Unit* getSelectedUnit();
@@ -51,6 +53,19 @@ public:
Unit* getUnitAt(int tx, int ty);
Unit* getUnitById(int id);
City* getCityAt(int tx, int ty);
City* getCityById(int id);
// City View System
bool isInCityView() const { return m_activeCityViewId != -1; }
int getActiveCityViewId() const { return m_activeCityViewId; }
City* getActiveCityView();
const City* getActiveCityView() const;
void enterCityView(int cityId);
void exitCityView();
CityYieldSummary calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
void commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandAttack(Unit *target);
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
@@ -81,6 +96,7 @@ private:
std::vector<FloatingText> m_floatingTexts;
int m_selectedUnitId;
int m_activeCityViewId;
ActionMode m_actionMode;
float m_movementTickDuration; // Default 30.0s
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
View File
Binary file not shown.
+321 -18
View File
@@ -503,27 +503,33 @@ void QSceneRenderer::drawTickBar(QPainter &painter)
void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
{
m_activeActionBubbles.clear();
m_cityBannerHits.clear();
painter.setRenderHint(QPainter::Antialiasing, true);
Unit *selectedUnit = GameManager::instance().getSelectedUnit();
// 1. Draw City Banners
for (const auto &c : GameManager::instance().getCities()) {
bool inFront = false;
QPointF screenP = UnitRenderer::projectToScreen(c.worldPos + QVector3D(0, 0, 0.65f), Projection, vMatrix, width(), height(), inFront);
if (!inFront) continue;
// 1. Draw City Banners (if not in city view)
if (!GameManager::instance().isInCityView()) {
for (const auto &c : GameManager::instance().getCities()) {
bool inFront = false;
QPointF screenP = UnitRenderer::projectToScreen(c.worldPos + QVector3D(0, 0, 0.65f), Projection, vMatrix, width(), height(), inFront);
if (!inFront) continue;
const Player *pl = GameManager::instance().players().getPlayer(c.playerId);
QColor col = pl ? pl->primaryColor : Qt::blue;
const Player *pl = GameManager::instance().players().getPlayer(c.playerId);
QColor col = pl ? pl->primaryColor : Qt::blue;
QRect bannerRect((int)screenP.x() - 40, (int)screenP.y() - 14, 80, 20);
painter.setBrush(QColor(20, 24, 34, 230));
painter.setPen(QPen(col, 1.8));
painter.drawRoundedRect(bannerRect, 4, 4);
QRect bannerRect((int)screenP.x() - 55, (int)screenP.y() - 14, 110, 22);
painter.setBrush(QColor(20, 24, 34, 235));
painter.setPen(QPen(col, 2.0));
painter.drawRoundedRect(bannerRect, 4, 4);
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
painter.setPen(Qt::white);
painter.drawText(bannerRect, Qt::AlignCenter, c.name);
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
painter.setPen(Qt::white);
QString bannerText = QString("%1 (%2 Pop)").arg(c.name).arg(c.getTotalPop());
painter.drawText(bannerRect, Qt::AlignCenter, bannerText);
m_cityBannerHits.push_back({bannerRect, c.id});
}
}
// 2. Draw Units: Teardrop (Druppel in secondary color) + Icon (in primary color)
@@ -874,8 +880,293 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
}
}
void QSceneRenderer::drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix)
{
painter.setRenderHint(QPainter::Antialiasing, true);
for (const auto &city : GameManager::instance().getCities()) {
const Player *player = GameManager::instance().players().getPlayer(city.playerId);
QColor ribbonColor = player ? player->primaryColor : QColor(255, 215, 60);
for (const auto &t : city.territory) {
int tx = t.first;
int ty = t.second;
QVector2D center = Pathfinder::hexToWorld(tx, ty);
for (int e = 0; e < 6; ++e) {
int nx, ny;
getHexNeighbor(tx, ty, e, nx, ny);
if (!city.ownsTile(nx, ny)) {
QVector2D p1, p2;
getHexEdgeEndpoints(e, p1, p2);
float wx1 = center.x() + p1.x();
float wy1 = center.y() + p1.y();
float wz1 = Unit::getTerrainHeight(wx1, wy1, m_point, m_numPoints05) + 0.035f;
float wx2 = center.x() + p2.x();
float wy2 = center.y() + p2.y();
float wz2 = Unit::getTerrainHeight(wx2, wy2, m_point, m_numPoints05) + 0.035f;
bool inFront1 = false, inFront2 = false;
QPointF s1 = UnitRenderer::projectToScreen(QVector3D(wx1, wy1, wz1), Projection, vMatrix, width(), height(), inFront1);
QPointF s2 = UnitRenderer::projectToScreen(QVector3D(wx2, wy2, wz2), Projection, vMatrix, width(), height(), inFront2);
if (inFront1 && inFront2) {
QColor glow = ribbonColor;
glow.setAlpha(90);
painter.setPen(QPen(glow, 5.5f, Qt::SolidLine, Qt::RoundCap));
painter.drawLine(s1, s2);
painter.setPen(QPen(ribbonColor, 2.6f, Qt::SolidLine, Qt::RoundCap));
painter.drawLine(s1, s2);
}
}
}
}
}
}
void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
{
m_citySlotHits.clear();
City *city = GameManager::instance().getActiveCityView();
if (!city) return;
painter.setRenderHint(QPainter::Antialiasing, true);
const Player *player = GameManager::instance().players().getPlayer(city->playerId);
QColor primaryCol = player ? player->primaryColor : Qt::blue;
QColor secondaryCol = player ? player->secondaryColor : Qt::white;
int maxRadius = TerrainYieldManager::instance().getCityConfig().maxWorkableRadius; // 3 deep hexagons
// 1. Render all hexes up to 3 distance around the city
for (int dx = -maxRadius - 1; dx <= maxRadius + 1; ++dx) {
for (int dy = -maxRadius - 1; dy <= maxRadius + 1; ++dy) {
int hx = city->tileX + dx;
int hy = city->tileY + dy;
if (hx < 0 || hx >= MAP_WIDTH || hy < 0 || hy >= MAP_HEIGHT) continue;
int dist = Pathfinder::hexDistance(city->tileX, city->tileY, hx, hy);
if (dist > maxRadius) continue;
QVector2D center = Pathfinder::hexToWorld(hx, hy);
float gz = Unit::getTerrainHeight(center.x(), center.y(), m_point, m_numPoints05);
bool inFront = false;
QPointF sCenter = UnitRenderer::projectToScreen(QVector3D(center.x(), center.y(), gz + 0.05f), Projection, vMatrix, width(), height(), inFront);
if (!inFront) continue;
bool isOwned = city->ownsTile(hx, hy);
// Draw hexagon boundary outline
QPolygonF hexPoly;
bool polyValid = true;
for (int e = 0; e < 6; ++e) {
QVector2D p1, p2;
getHexEdgeEndpoints(e, p1, p2);
float px = center.x() + p1.x();
float py = center.y() + p1.y();
float pz = Unit::getTerrainHeight(px, py, m_point, m_numPoints05) + 0.02f;
bool pInFront = false;
QPointF sp = UnitRenderer::projectToScreen(QVector3D(px, py, pz), Projection, vMatrix, width(), height(), pInFront);
if (!pInFront) { polyValid = false; break; }
hexPoly << sp;
}
if (polyValid) {
if (isOwned) {
painter.setPen(QPen(QColor(80, 200, 255, 140), 1.4, Qt::SolidLine));
painter.setBrush(QColor(40, 120, 200, 30));
} else {
painter.setPen(QPen(QColor(120, 130, 145, 90), 1.0, Qt::DashLine));
painter.setBrush(QColor(20, 25, 35, 40));
}
painter.drawPolygon(hexPoly);
}
// Check workability
QString tType = TerrainYieldManager::instance().getTerrainType(hx, hy, m_point, m_numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
if (isOwned) {
if (yCfg && yCfg->workable) {
// Tile has 1 slot!
bool isWorked = city->isTileWorked(hx, hy);
QString res = isWorked ? city->getWorkedResource(hx, hy) : yCfg->primaryResource;
int amount = yCfg->getYield(res);
float slotR = 15.0f;
QRect slotRect((int)(sCenter.x() - slotR), (int)(sCenter.y() - slotR), (int)(slotR * 2), (int)(slotR * 2));
m_citySlotHits.push_back({slotRect, hx, hy, res});
// Slot circle background
painter.setBrush(isWorked ? QColor(25, 55, 38, 245) : QColor(18, 25, 36, 225));
painter.setPen(QPen(isWorked ? QColor(80, 240, 120) : QColor(140, 175, 215), isWorked ? 2.2 : 1.2));
painter.drawEllipse(sCenter, slotR, slotR);
// Resource icon
QImage rIcon = getTintedIcon("images/resources/" + res + ".png", isWorked ? QColor(80, 255, 120) : QColor(180, 210, 245));
painter.drawImage(QRectF(sCenter.x() - 8.5, sCenter.y() - 8.5, 17, 17), rIcon);
// Yield pill below slot
QRect pillRect((int)sCenter.x() - 26, (int)sCenter.y() + 16, 52, 14);
painter.setBrush(QColor(15, 20, 28, 220));
painter.setPen(QPen(isWorked ? QColor(80, 240, 120, 160) : QColor(80, 110, 140, 120), 1.0));
painter.drawRoundedRect(pillRect, 3, 3);
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
painter.setPen(isWorked ? QColor(100, 255, 140) : QColor(180, 205, 235));
painter.drawText(pillRect, Qt::AlignCenter, QString("+%1").arg(amount));
// If unworked and pop is available: show small green +
if (!isWorked && city->getAvailablePop() > 0) {
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.008f) + 1.0f) * 0.5f;
painter.setBrush(QColor(60, 220, 90, (int)(180 + pulse * 75)));
painter.setPen(Qt::NoPen);
painter.drawEllipse(QPointF(sCenter.x() + 10, sCenter.y() - 10), 4.5, 4.5);
}
}
// If not workable (e.g. Sneeuw / Snow): no slot is shown, as requested!
} else {
// Outside borders: draw subtle locked badge
QRect lockRect((int)sCenter.x() - 12, (int)sCenter.y() - 7, 24, 14);
painter.setBrush(QColor(20, 24, 32, 180));
painter.setPen(QPen(QColor(100, 120, 140, 100), 1.0));
painter.drawRoundedRect(lockRect, 3, 3);
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
painter.setPen(QColor(140, 155, 175));
painter.drawText(lockRect, Qt::AlignCenter, "SLOT");
}
}
}
// 2. City Management Top Card
int cardW = 500, cardH = 145;
QRect cardRect((width() - cardW) / 2, 16, cardW, cardH);
painter.setBrush(QColor(16, 22, 32, 242));
painter.setPen(QPen(primaryCol, 1.8));
painter.drawRoundedRect(cardRect, 10, 10);
// City Title & Close Button
painter.setFont(QFont("sans-serif", 13, QFont::Bold));
painter.setPen(QColor(245, 250, 255));
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 10, 260, 24), Qt::AlignLeft | Qt::AlignVCenter, city->name.toUpper() + " - STADSBEHEER");
// Close button [X]
m_closeCityViewBtnRect = QRect(cardRect.right() - 88, cardRect.top() + 10, 74, 24);
painter.setBrush(QColor(40, 50, 68, 220));
painter.setPen(QPen(secondaryCol, 1.2));
painter.drawRoundedRect(m_closeCityViewBtnRect, 4, 4);
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
painter.setPen(QColor(220, 235, 255));
painter.drawText(m_closeCityViewBtnRect, Qt::AlignCenter, "[X] Sluiten");
// Population Stats Row
int totalPop = city->getTotalPop();
int assigned = city->getAssignedPop();
int avail = city->getAvailablePop();
painter.setFont(QFont("sans-serif", 9));
painter.setPen(QColor(180, 205, 235));
QString popInfo = QString("Inwoners: %L1 | Totale Pop: %2 | Aan het werk: %3 | Beschikbaar: %4")
.arg(city->inhabitants).arg(totalPop).arg(assigned).arg(avail);
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 38, cardRect.width() - 32, 18), Qt::AlignLeft | Qt::AlignVCenter, popInfo);
// Separator line
painter.setPen(QPen(QColor(60, 85, 120, 150), 1.0));
painter.drawLine(cardRect.left() + 16, cardRect.top() + 60, cardRect.right() - 16, cardRect.top() + 60);
// Total Yields Summary Row
CityYieldSummary summary = GameManager::instance().calculateCityYields(*city, m_point, m_numPoints05);
struct YieldItem { QString name; int val; QString icon; QColor col; };
std::vector<YieldItem> yields = {
{"Voedsel", summary.food, "images/resources/voedsel.png", QColor(100, 230, 110)},
{"Arbeid", summary.labor, "images/resources/arbeid.png", QColor(240, 140, 50)},
{"Kennis", summary.science, "images/resources/kennis.png", QColor(60, 190, 255)},
{"Cultuur", summary.culture, "images/resources/cultuur.png", QColor(200, 110, 240)},
{"Geloof", summary.faith, "images/resources/geloof.png", QColor(255, 230, 80)},
{"Goud", summary.gold, "images/resources/goud.png", QColor(255, 210, 50)}
};
int colW = (cardRect.width() - 32) / 6;
for (size_t i = 0; i < yields.size(); ++i) {
int cx = cardRect.left() + 16 + (int)i * colW;
QRect yRect(cx, cardRect.top() + 68, colW - 6, 44);
painter.setBrush(QColor(12, 16, 24, 180));
painter.setPen(QPen(QColor(40, 60, 85, 120), 1.0));
painter.drawRoundedRect(yRect, 4, 4);
QImage icon = getTintedIcon(yields[i].icon, yields[i].col);
painter.drawImage(QRect(yRect.left() + 6, yRect.top() + 6, 16, 16), icon);
painter.setFont(QFont("sans-serif", 7));
painter.setPen(QColor(160, 180, 205));
painter.drawText(QRect(yRect.left() + 26, yRect.top() + 6, yRect.width() - 28, 16), Qt::AlignLeft | Qt::AlignVCenter, yields[i].name);
painter.setFont(QFont("sans-serif", 9, QFont::Bold));
painter.setPen(yields[i].col);
painter.drawText(QRect(yRect.left() + 6, yRect.top() + 24, yRect.width() - 12, 16), Qt::AlignLeft | Qt::AlignVCenter, QString("+%1").arg(yields[i].val));
}
// Help instruction at bottom
painter.setFont(QFont("sans-serif", 7, QFont::StyleItalic));
painter.setPen(QColor(140, 170, 200));
painter.drawText(QRect(cardRect.left() + 16, cardRect.bottom() - 20, cardRect.width() - 32, 16), Qt::AlignCenter,
"Klik op een tegel binnen de grenzen om 1 Pop toe te wijzen of vrij te maken.");
}
void QSceneRenderer::mousePressEvent(QMouseEvent *event)
{
// 0. If in City View: handle close button and slot clicks
if (GameManager::instance().isInCityView()) {
if (m_closeCityViewBtnRect.contains(event->pos())) {
GameManager::instance().exitCityView();
update();
return;
}
for (const auto &slot : m_citySlotHits) {
if (slot.rect.contains(event->pos())) {
City *city = GameManager::instance().getActiveCityView();
if (city) {
QVector2D w = Pathfinder::hexToWorld(slot.tx, slot.ty);
float gz = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05);
if (city->isTileWorked(slot.tx, slot.ty)) {
city->unassignWorker(slot.tx, slot.ty);
GameManager::instance().addFloatingText("Pop vrijgemaakt", QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 220, 80));
} else {
if (city->getAvailablePop() > 0) {
city->assignWorker(slot.tx, slot.ty, slot.defaultRes);
GameManager::instance().addFloatingText("+1 Pop aan het werk", QVector3D(w.x(), w.y(), gz + 0.8f), QColor(80, 255, 120));
} else {
GameManager::instance().addFloatingText("Geen vrije Pop!", QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 80, 80));
}
}
update();
return;
}
}
}
}
// Check if clicked on a City Banner to open City View
for (const auto &cb : m_cityBannerHits) {
if (cb.rect.contains(event->pos())) {
GameManager::instance().enterCityView(cb.cityId);
City *c = GameManager::instance().getCityById(cb.cityId);
if (c) {
cameraPosition.setX(c->worldPos.x());
cameraPosition.setY(c->worldPos.y() - 2.8f);
}
update();
return;
}
}
// 1. Zoom Out / Reset / In buttons
if (getZoomOutButtonRect().contains(event->pos())) {
m_minimapZoom = std::max(1.0f, m_minimapZoom / 1.5f);
@@ -1010,8 +1301,15 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
Unit *unitOnTile = GameManager::instance().getUnitAt(hitX, hitY);
if (unitOnTile) {
GameManager::instance().selectUnit(unitOnTile->id);
} else if (!m_unitCardRect.contains(event->pos())) {
GameManager::instance().deselectUnit();
} else {
City *cityOnTile = GameManager::instance().getCityAt(hitX, hitY);
if (cityOnTile) {
GameManager::instance().enterCityView(cityOnTile->id);
cameraPosition.setX(cityOnTile->worldPos.x());
cameraPosition.setY(cityOnTile->worldPos.y() - 2.8f);
} else if (!m_unitCardRect.contains(event->pos())) {
GameManager::instance().deselectUnit();
}
}
update();
return;
@@ -1103,7 +1401,9 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e)
GameManager::instance().commandBuildCitySelected(m_point, m_numPoints05);
}
else if (e->key() == Qt::Key_Escape) {
if (GameManager::instance().currentActionMode() != ActionMode::None) {
if (GameManager::instance().isInCityView()) {
GameManager::instance().exitCityView();
} else if (GameManager::instance().currentActionMode() != ActionMode::None) {
GameManager::instance().setActionMode(ActionMode::None);
} else {
GameManager::instance().deselectUnit();
@@ -1243,13 +1543,16 @@ void QSceneRenderer::paintGL()
// 3. Render 2D UI & HUD Overlays via QPainter
QPainter painter(this);
drawCityBorders(painter, vMatrix);
drawPathAndRange(painter, selected, vMatrix);
drawMinimap(painter);
drawTickBar(painter);
drawUnitsOverlay(painter, vMatrix);
drawFloatingTexts(painter, vMatrix);
if (selected) {
if (GameManager::instance().isInCityView()) {
drawCityViewOverlay(painter, vMatrix);
} else if (selected) {
drawUnitCard(painter, selected);
}
painter.end();
+17
View File
@@ -54,6 +54,9 @@ protected:
void drawTickBar(QPainter &painter);
void drawPathAndRange(QPainter &painter, Unit *unit, const QMatrix4x4 &vMatrix);
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
// City & Territory Overlays
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
QImage getTintedIcon(const QString &path, const QColor &color);
@@ -95,6 +98,20 @@ protected:
QRect m_speed2xRect;
QRect m_speed5xRect;
QRect m_unitCardRect;
QRect m_closeCityViewBtnRect;
struct CitySlotHit {
QRect rect;
int tx, ty;
QString defaultRes;
};
std::vector<CitySlotHit> m_citySlotHits;
struct CityBannerHit {
QRect rect;
int cityId;
};
std::vector<CityBannerHit> m_cityBannerHits;
};
#endif // QSCENERENDERER_H
BIN
View File
Binary file not shown.
+21
View File
@@ -41,6 +41,27 @@ inline QVector2D getHexEdgeMidpoint(int edge) {
return midpoints[(edge % 6 + 6) % 6];
}
// Returns the two corner endpoints in local tile coordinates for a given edge (radius = 0.5)
inline void getHexEdgeEndpoints(int edge, QVector2D &p1, QVector2D &p2) {
static const QVector2D corners[6] = {
QVector2D(-0.25f, 0.43301270f), // 0: Top-Left
QVector2D( 0.25f, 0.43301270f), // 1: Top-Right
QVector2D( 0.5f, 0.0f), // 2: Right
QVector2D( 0.25f, -0.43301270f), // 3: Bottom-Right
QVector2D(-0.25f, -0.43301270f), // 4: Bottom-Left
QVector2D(-0.5f, 0.0f) // 5: Left
};
switch ((edge % 6 + 6) % 6) {
case 0: p1 = corners[5]; p2 = corners[0]; break;
case 1: p1 = corners[0]; p2 = corners[1]; break;
case 2: p1 = corners[1]; p2 = corners[2]; break;
case 3: p1 = corners[2]; p2 = corners[3]; break;
case 4: p1 = corners[3]; p2 = corners[4]; break;
case 5: p1 = corners[4]; p2 = corners[5]; break;
default: p1 = corners[0]; p2 = corners[1]; break;
}
}
// Returns the coordinates of the neighbor tile sharing the given edge
inline void getHexNeighbor(int x, int y, int edge, int &nx, int &ny) {
bool even = (x % 2 == 0);
+107
View File
@@ -0,0 +1,107 @@
#include "terrainyieldmanager.h"
#include "yamlparser.h"
#include "pathfinder.h"
#include <QDebug>
#include <cmath>
TerrainYieldManager& TerrainYieldManager::instance()
{
static TerrainYieldManager s_instance;
return s_instance;
}
TerrainYieldManager::TerrainYieldManager()
: m_loaded(false)
{
}
bool TerrainYieldManager::loadConfigs(const QString &baseDir)
{
// 1. Load city.yaml
QString cityFile = baseDir + "/config/city.yaml";
YamlNode cityRoot = YamlParser::parseFile(cityFile);
if (!cityRoot.children.isEmpty()) {
m_cityConfig.initialInhabitants = cityRoot["initial_inhabitants"].asInt(1000);
m_cityConfig.inhabitantsPerPop = cityRoot["inhabitants_per_pop"].asInt(100000);
m_cityConfig.initialBorderRadius = cityRoot["initial_border_radius"].asInt(1);
m_cityConfig.maxWorkableRadius = cityRoot["max_workable_radius"].asInt(3);
m_cityConfig.defaultTileSlots = cityRoot["default_tile_slots"].asInt(1);
}
// 2. Load terrain_yields.yaml
QString yieldsFile = baseDir + "/config/terrain_yields.yaml";
YamlNode yieldsRoot = YamlParser::parseFile(yieldsFile);
for (auto it = yieldsRoot.children.begin(); it != yieldsRoot.children.end(); ++it) {
QString typeKey = it.key();
const YamlNode &node = it.value();
TerrainYieldConfig cfg;
cfg.id = typeKey;
cfg.name = node["name"].asString(typeKey);
cfg.workable = node["workable"].asBool(true);
cfg.defaultSlots = node["default_slots"].asInt(cfg.workable ? 1 : 0);
cfg.primaryResource = node["primary_resource"].asString("voedsel");
if (node.hasChild("yields")) {
const YamlNode &yNode = node["yields"];
for (auto yIt = yNode.children.begin(); yIt != yNode.children.end(); ++yIt) {
cfg.yields[yIt.key()] = yIt.value().asInt(0);
}
}
m_yieldConfigs[typeKey] = cfg;
}
m_loaded = true;
qDebug() << "TerrainYieldManager loaded" << m_yieldConfigs.size() << "terrain types and city config.";
return true;
}
QString TerrainYieldManager::getTerrainType(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
{
if (!mapPoints || numPoints05 <= 0) return "grasland";
QVector2D w = Pathfinder::hexToWorld(tx, ty);
float scaleX = numPoints05 * 2.0f;
float scaleY = numPoints05 / LOD_0_SIZE_Y;
int mapX = (int) std::round(w.x() * scaleX);
int mapY = (int) std::round(w.y() * scaleY);
if (mapX < 0) mapX = 0;
if (mapX >= (int)mapPoints->getWidth()) mapX = (int)mapPoints->getWidth() - 1;
if (mapY < 0) mapY = 0;
if (mapY >= (int)mapPoints->getHeight()) mapY = (int)mapPoints->getHeight() - 1;
TerrainPoint tp = mapPoints->operator()(mapX, mapY);
// Sea level water
if (tp.height < 6) return "water";
float heightKm = (tp.height / 255.0f) * 3.5f + 1.0f;
// High peaks have snow
if (heightKm >= 3.1f) return "sneeuw";
// Mountain rock
if (heightKm >= 2.3f) return "steen";
switch (tp.type) {
case 0: return "grasland";
case 1: return "sahara";
case 2: return "tundra";
case 3: return "red_canyon";
case 4: return "bos";
default: return "grasland";
}
}
const TerrainYieldConfig* TerrainYieldManager::getYieldConfig(const QString &terrainType) const
{
auto it = m_yieldConfigs.find(terrainType);
if (it != m_yieldConfigs.end()) {
return &it.value();
}
return nullptr;
}
+57
View File
@@ -0,0 +1,57 @@
#ifndef TERRAINYIELDMANAGER_H
#define TERRAINYIELDMANAGER_H
#include <QString>
#include <QMap>
#include <vector>
#include "terrain.h"
#include "matrix2d.h"
struct TerrainYieldConfig {
QString id;
QString name;
bool workable;
int defaultSlots;
QString primaryResource;
QMap<QString, int> yields; // "voedsel", "arbeid", "kennis", "cultuur", "geloof", "goud"
TerrainYieldConfig()
: id(""), name(""), workable(true), defaultSlots(1), primaryResource("voedsel") {}
int getYield(const QString &res) const {
return yields.value(res, 0);
}
};
struct CityConfig {
int initialInhabitants;
int inhabitantsPerPop;
int initialBorderRadius;
int maxWorkableRadius;
int defaultTileSlots;
CityConfig()
: initialInhabitants(1000), inhabitantsPerPop(100000),
initialBorderRadius(1), maxWorkableRadius(3), defaultTileSlots(1) {}
};
class TerrainYieldManager {
public:
static TerrainYieldManager& instance();
bool loadConfigs(const QString &baseDir);
QString getTerrainType(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
const TerrainYieldConfig* getYieldConfig(const QString &terrainType) const;
const CityConfig& getCityConfig() const { return m_cityConfig; }
private:
TerrainYieldManager();
~TerrainYieldManager() = default;
QMap<QString, TerrainYieldConfig> m_yieldConfigs;
CityConfig m_cityConfig;
bool m_loaded;
};
#endif // TERRAINYIELDMANAGER_H
Binary file not shown.
BIN
View File
Binary file not shown.
+3
View File
@@ -25,6 +25,8 @@ SOURCES += main.cpp \
unitconfig.cpp \
pathfinder.cpp \
unit.cpp \
city.cpp \
terrainyieldmanager.cpp \
gamemanager.cpp \
unitrenderer.cpp
@@ -42,6 +44,7 @@ HEADERS += mainwindow.h \
pathfinder.h \
unit.h \
city.h \
terrainyieldmanager.h \
gamemanager.h \
unitrenderer.h
BIN
View File
Binary file not shown.
+43 -12
View File
@@ -1,6 +1,7 @@
#include "yamlparser.h"
#include <QFile>
#include <QTextStream>
#include <vector>
YamlNode YamlParser::parseFile(const QString &filePath)
{
@@ -16,26 +17,56 @@ YamlNode YamlParser::parseString(const QString &content)
{
YamlNode root;
QStringList lines = content.split('\n');
QString currentKey = "";
struct StackEntry {
int indent;
YamlNode *node;
};
std::vector<StackEntry> stack;
stack.push_back({-1, &root});
for (int i = 0; i < lines.size(); ++i) {
QString line = lines[i];
QString rawLine = lines[i];
// Strip comments
int commentIdx = line.indexOf('#');
int commentIdx = rawLine.indexOf('#');
if (commentIdx != -1) {
line = line.left(commentIdx);
rawLine = rawLine.left(commentIdx);
}
QString trimmed = line.trimmed();
// Calculate leading indentation spaces (tabs expanded to 2 spaces)
int indent = 0;
int idx = 0;
while (idx < rawLine.length()) {
if (rawLine[idx] == ' ') {
indent++;
idx++;
} else if (rawLine[idx] == '\t') {
indent += 2;
idx++;
} else {
break;
}
}
QString trimmed = rawLine.trimmed();
if (trimmed.isEmpty()) continue;
// Pop stack until parent has smaller indent
while (stack.size() > 1 && stack.back().indent >= indent) {
stack.pop_back();
}
YamlNode *parent = stack.back().node;
// Check for list item: "- something"
if (trimmed.startsWith("- ")) {
QString itemVal = trimmed.mid(2).trimmed();
if (!currentKey.isEmpty()) {
root.children[currentKey].listValues.append(itemVal);
if ((itemVal.startsWith('"') && itemVal.endsWith('"')) || (itemVal.startsWith('\'') && itemVal.endsWith('\''))) {
itemVal = itemVal.mid(1, itemVal.length() - 2);
}
parent->listValues.append(itemVal);
continue;
}
@@ -45,15 +76,15 @@ YamlNode YamlParser::parseString(const QString &content)
QString key = trimmed.left(colonIdx).trimmed();
QString val = trimmed.mid(colonIdx + 1).trimmed();
// Strip enclosing quotes if any
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith('\'') && val.endsWith('\''))) {
val = val.mid(1, val.length() - 2);
}
currentKey = key;
YamlNode node;
node.value = val;
root.children[key] = node;
parent->children[key] = YamlNode();
YamlNode *newNode = &(parent->children[key]);
newNode->value = val;
stack.push_back({indent, newNode});
}
}
BIN
View File
Binary file not shown.