stad
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 403 B |
|
After Width: | Height: | Size: 291 B |
|
After Width: | Height: | Size: 437 B |
|
After Width: | Height: | Size: 485 B |
|
After Width: | Height: | Size: 549 B |
|
After Width: | Height: | Size: 417 B |
|
After Width: | Height: | Size: 388 B |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||