Refactor farm building speed to builder unit action_speeds and enable infinite charges

This commit is contained in:
bart
2026-11-08 17:01:26 +01:00
parent 55e4276025
commit 17901ce25c
10 changed files with 847 additions and 121 deletions
+15
View File
@@ -0,0 +1,15 @@
# Tile improvements configuration
# All values must be configurable via YAML as per AGENTS.md
farm:
id: farm
name: "Boerderij"
action: "boerderij_bouwen"
texture: "images/farm.jpeg"
texture_scale: 0.6 # World-space scale: 0.6 spans nicely across adjacent hexes without repeating unnaturally
texture_mapping: "world" # "world" = seamless across adjacent hex tiles; "hex" = centered per tile
valid_terrains:
- grasland
- tundra
yield_bonus:
voedsel: 1000
+15
View File
@@ -0,0 +1,15 @@
id: builder
name: Builder
category: bouwen
domain: land
health: 100
movement_speed: 2.0
cost: 50
build_charges: -1 # -1 voor oneindig doorwerken
infinite_charges: true
icon: images/units/builder.png
actions:
- verplaatsen
- boerderij_bouwen
action_speeds:
boerderij_bouwen: 10.0 # Duur in seconden die deze builder unit nodig heeft voor de actie boerderij_bouwen
+241 -3
View File
@@ -2,6 +2,8 @@
#include "pathfinder.h"
#include "yamlparser.h"
#include "terrainyieldmanager.h"
#include "terrainmovementmanager.h"
#include "localization.h"
#include <QDebug>
#include <cmath>
@@ -23,6 +25,9 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
{
if (m_initialized) return;
// Load localization
Localization::instance().loadFile(baseDir + "/config/strings_en.yaml");
// Load terrain yields & city configuration
TerrainYieldManager::instance().loadConfigs(baseDir);
@@ -37,8 +42,17 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
QString unitsConfigDir = baseDir + "/config/units";
m_unitConfigManager.loadFromDirectory(unitsConfigDir);
// Spawn an initial established city for Player 1 at (35, 17)
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
// Initialize player national resources from configuration
for (auto &player : m_playerManager.getPlayersMutable()) {
player.culture = cCfg.initialNationalResources.value("cultuur", 10000);
player.science = cCfg.initialNationalResources.value("kennis", 0);
player.faith = cCfg.initialNationalResources.value("geloof", 0);
player.gold = cCfg.initialNationalResources.value("goud", 0);
}
// Spawn an initial established city for Player 1 at (35, 17)
int cId = getNextCityId();
QVector2D w = Pathfinder::hexToWorld(35, 17);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
@@ -57,8 +71,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
// Spawn initial demo units near starting camera position
// Player 1 (Blue & Gold)
spawnUnit("settler", 1, 36, 18, mapPoints, numPoints05);
spawnUnit("builder", 1, 35, 18, mapPoints, numPoints05);
spawnUnit("warrior", 1, 37, 18, mapPoints, numPoints05);
spawnUnit("slinger", 1, 37, 19, mapPoints, numPoints05);
spawnUnit("galley", 1, 41, 92, mapPoints, numPoints05);
// Player 2 (Red & White) - Opponent units
spawnUnit("warrior", 2, 40, 18, mapPoints, numPoints05);
@@ -92,12 +108,16 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
m_tickTimer += deltaTime * m_simSpeedMultiplier;
if (m_tickTimer >= m_movementTickDuration) {
m_tickTimer = std::fmod(m_tickTimer, m_movementTickDuration);
processTickYields(mapPoints, numPoints05);
}
// Update units
for (auto &u : m_units) {
if (!u->isDead()) {
u->update(deltaTime, m_movementTickDuration, m_simSpeedMultiplier, mapPoints, numPoints05);
if (u->isBuilding() && u->buildProgress >= 1.0f) {
completeUnitBuilding(*u, mapPoints, numPoints05);
}
}
}
@@ -214,6 +234,17 @@ void GameManager::exitCityView()
CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
{
CityYieldSummary summary;
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
// Base yields for every city per tick
summary.food += cCfg.getBaseYield("voedsel");
summary.labor += cCfg.getBaseYield("arbeid");
summary.science += cCfg.getBaseYield("kennis");
summary.culture += cCfg.getBaseYield("cultuur");
summary.faith += cCfg.getBaseYield("geloof");
summary.gold += cCfg.getBaseYield("goud");
// Additional yields from worked tiles
for (const auto &pair : city.workedTiles) {
int tx = pair.first.first;
int ty = pair.first.second;
@@ -224,6 +255,16 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
if (!yCfg) continue;
int amount = yCfg->getYield(assignedRes);
// Add bonus from tile improvements (e.g. farm bonus)
const TileImprovement *imp = getImprovementAt(tx, ty);
if (imp) {
const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId);
if (iCfg && iCfg->yieldBonus.contains(assignedRes)) {
amount += iCfg->yieldBonus.value(assignedRes, 0);
}
}
if (assignedRes == "voedsel") summary.food += amount;
else if (assignedRes == "arbeid") summary.labor += amount;
else if (assignedRes == "kennis") summary.science += amount;
@@ -234,6 +275,88 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
return summary;
}
void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig();
for (auto &city : m_cities) {
CityYieldSummary yields = calculateCityYields(city, mapPoints, numPoints05);
// 1. Food is converted into inhabitants:
int foodGrown = (int)std::round(yields.food * cfg.foodToInhabitantsRatio);
city.inhabitants += foodGrown;
// 2. Labor is accumulated locally in the city:
city.accumulatedLabor += yields.labor;
// 3. National resources go to the owner player:
Player *owner = m_playerManager.getPlayer(city.playerId);
if (owner) {
owner->culture += yields.culture;
owner->science += yields.science;
owner->faith += yields.faith;
owner->gold += yields.gold;
}
// Floating growth text over city
QVector3D ftPos = city.worldPos + QVector3D(0.0f, 0.0f, 1.1f);
addFloatingText(QString("+%1 Inwoners | +%2 Cultuur").arg(foodGrown).arg(yields.culture),
ftPos, QColor(100, 255, 140), 2.5f);
}
}
bool GameManager::isTileOwned(int tx, int ty, int *outOwnerCityId) const
{
for (const auto &c : m_cities) {
if (c.ownsTile(tx, ty)) {
if (outOwnerCityId) *outOwnerCityId = c.id;
return true;
}
}
return false;
}
bool GameManager::buyTileForCity(int cityId, int tx, int ty)
{
City *city = getCityById(cityId);
if (!city) return false;
// Check if tile is already owned by any city
int existingOwnerId = -1;
if (isTileOwned(tx, ty, &existingOwnerId)) {
return false;
}
const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig();
// Check distance (must be within max workable radius)
int dist = Pathfinder::hexDistance(city->tileX, city->tileY, tx, ty);
if (dist > cfg.maxWorkableRadius) {
return false;
}
// Check culture cost
Player *player = m_playerManager.getPlayer(city->playerId);
if (!player) return false;
int cost = cfg.tileCultureCost;
if (player->culture < cost) {
QVector2D w = Pathfinder::hexToWorld(tx, ty);
addFloatingText(QString("Niet genoeg Cultuur! (%1 nodig)").arg(cost),
QVector3D(w.x(), w.y(), 0.8f), QColor(255, 90, 90), 2.0f);
return false;
}
// Deduct culture and claim tile
player->culture -= cost;
city->addTerritory(tx, ty);
QVector2D w = Pathfinder::hexToWorld(tx, ty);
addFloatingText(QString("Tegel gekocht! (-%1 Cultuur)").arg(cost),
QVector3D(w.x(), w.y(), 0.8f), QColor(210, 120, 255), 2.5f);
return true;
}
void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
@@ -243,13 +366,31 @@ void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<Terrain
return;
}
// Check if target tile is passable for this unit's movement domain
if (!TerrainMovementManager::instance().isHexPassable(
targetX, targetY, selected->config.movementDomain, mapPoints, numPoints05, &selected->config)) {
if (selected->isNaval()) {
addFloatingText(Localization::instance().get("ui.unit.naval_only_water", "Ships can only sail on water!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
} else {
addFloatingText(Localization::instance().get("ui.unit.impassable_terrain", "Impassable terrain!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
}
return;
}
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05
selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05,
selected->config.movementDomain, &selected->config
);
if (path.size() > 1) {
selected->setPath(path);
addFloatingText("Verplaatsen", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
} else {
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
}
}
@@ -318,3 +459,100 @@ void GameManager::addFloatingText(const QString &text, const QVector3D &worldPos
{
m_floatingTexts.push_back({text, worldPos, duration, duration, color});
}
const TileImprovement* GameManager::getImprovementAt(int tx, int ty) const
{
auto it = m_tileImprovements.find({tx, ty});
if (it != m_tileImprovements.end()) {
return &it->second;
}
return nullptr;
}
bool GameManager::hasImprovementAt(int tx, int ty) const
{
return m_tileImprovements.find({tx, ty}) != m_tileImprovements.end();
}
void GameManager::commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
if (!selected || !selected->config.canBuildFarm()) return;
if (selected->isBuilding()) {
addFloatingText("Al bezig met bouwen!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 200, 70));
return;
}
int tx = selected->tileX;
int ty = selected->tileY;
// Check if tile already has an improvement
if (hasImprovementAt(tx, ty)) {
addFloatingText("Er staat hier al een boerderij!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
// Check terrain type against allowed terrains loaded from improvements.yaml
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig("farm");
QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"};
if (!validTerrains.contains(tType)) {
addFloatingText("Kan hier niet bouwen! Alleen op gras en toendra.", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
// Start building process (duration comes from the builder unit's action speed)
float duration = selected->config.getActionDuration("boerderij_bouwen", 10.0f);
selected->startBuilding("farm", duration);
addFloatingText("Boerderij in aanbouw...", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
}
void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
int tx = unit.tileX;
int ty = unit.tileY;
QString impId = unit.buildingImprovementId.isEmpty() ? "farm" : unit.buildingImprovementId;
if (hasImprovementAt(tx, ty)) {
unit.cancelBuilding();
return;
}
// Place improvement now that it is completed!
QVector2D w = Pathfinder::hexToWorld(tx, ty);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
TileImprovement imp;
imp.improvementId = impId;
imp.tileX = tx;
imp.tileY = ty;
imp.playerId = unit.playerId;
imp.worldPos = QVector3D(w.x(), w.y(), gz);
m_tileImprovements[{tx, ty}] = imp;
unit.cancelBuilding();
if (!unit.config.infiniteCharges && unit.buildCharges > 0) {
unit.buildCharges -= 1;
if (unit.buildCharges <= 0) {
addFloatingText("Boerderij voltooid! (Builder verbruikt)", QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
unit.takeDamage(99999.0f);
if (unit.id == m_selectedUnitId) {
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
return;
} else {
QString msg = QString("Boerderij voltooid! (%1 %2 over)")
.arg(unit.buildCharges)
.arg(unit.buildCharges == 1 ? "lading" : "ladingen");
addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
return;
}
}
addFloatingText("Boerderij voltooid!", QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
}
+359 -71
View File
@@ -6,6 +6,7 @@
#include <QToolTip>
#include <QDebug>
#include "pathfinder.h"
#include "yamlparser.h"
GLuint vertexbuffer;
GLuint VertexArrayID;
@@ -46,6 +47,7 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
}
}
loadCameraConfig();
zRot = 90.0f; // Default looking North (+Y)
m_zoom = 8.0f;
cameraUpDirection = cameraTransformation * QVector3D(0, 1, 0);
@@ -72,6 +74,46 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
animTimer->start(16);
}
void QSceneRenderer::loadCameraConfig()
{
QString configPath = "/home/bart/project/tiletest/config/game.yaml";
YamlNode root = YamlParser::parseFile(configPath);
if (root.hasChild("camera")) {
YamlNode cam = root["camera"];
m_cameraMinZoom = cam["min_zoom"].asFloat(m_cameraMinZoom);
m_cameraMaxZoom = cam["max_zoom"].asFloat(m_cameraMaxZoom);
m_cameraPitchMinDeg = cam["pitch_min_deg"].asFloat(m_cameraPitchMinDeg);
m_cameraPitchMaxDeg = cam["pitch_max_deg"].asFloat(m_cameraPitchMaxDeg);
}
}
float QSceneRenderer::getCameraPitchDeg() const
{
float range = m_cameraMaxZoom - m_cameraMinZoom;
float zoomT = range > 1e-4f ? std::max(0.0f, std::min(1.0f, (m_zoom - m_cameraMinZoom) / range)) : 0.0f;
return m_cameraPitchMinDeg + zoomT * (m_cameraPitchMaxDeg - m_cameraPitchMinDeg);
}
float QSceneRenderer::getCameraForwardDist() const
{
float pitchRad = getCameraPitchDeg() * PI / 180.0f;
float tanPitch = std::tan(pitchRad);
if (tanPitch < 1e-4f) tanPitch = 1e-4f;
return m_zoom / tanPitch;
}
QVector3D QSceneRenderer::getCameraLookAt() const
{
float forwardDist = getCameraForwardDist();
float rad = zRot * PI / 180.0f;
return QVector3D(
cameraPosition.x() + forwardDist * cos(rad),
cameraPosition.y() + forwardDist * sin(rad),
0.0f
);
}
void QSceneRenderer::loadRiverMap(const QImage &riverImg)
{
if (riverImg.isNull()) return;
@@ -323,7 +365,9 @@ void QSceneRenderer::drawMinimap(QPainter &painter)
float rad = zRot * PI / 180.0f;
float fovHalf = 25.0f * PI / 180.0f;
float viewDist = std::max(12.0f, m_zoom * 2.2f);
float pitchDeg = getCameraPitchDeg();
float farPitchRad = (pitchDeg - 22.5f) * PI / 180.0f;
float viewDist = std::max(4.0f, (float)(m_zoom / std::tan(farPitchRad)));
float lx = cameraPosition.x() + viewDist * cos(rad - fovHalf);
float ly = cameraPosition.y() + viewDist * sin(rad - fovHalf);
@@ -374,10 +418,11 @@ void QSceneRenderer::drawMinimap(QPainter &painter)
QRect bottomBar(cardRect.left() + 6, mapRect.bottom() + 4, cardRect.width() - 12, 16);
painter.setFont(QFont("sans-serif", 8));
painter.setPen(QColor(180, 200, 220));
QString info = QString("Cam: %1, %2 km | Alt: %3 km")
QString info = QString("Cam: %1, %2 km | Alt: %3 km | Hoek: %4°")
.arg(cameraPosition.x(), 0, 'f', 1)
.arg(cameraPosition.y(), 0, 'f', 1)
.arg(m_zoom, 0, 'f', 1);
.arg(m_zoom, 0, 'f', 1)
.arg(pitchDeg, 0, 'f', 0);
painter.drawText(bottomBar, Qt::AlignLeft | Qt::AlignVCenter, info);
}
@@ -452,15 +497,15 @@ void QSceneRenderer::drawTickBar(QPainter &painter)
{
painter.setRenderHint(QPainter::Antialiasing, true);
int barW = 340, barH = 34;
int barW = 690, barH = 34;
QRect barRect(16, 16, barW, barH);
// Dark glass card
painter.setBrush(QColor(18, 24, 34, 230));
painter.setBrush(QColor(18, 24, 34, 235));
painter.setPen(QPen(QColor(70, 100, 140, 180), 1.2));
painter.drawRoundedRect(barRect, 6, 6);
// Movement Tick Info
// 1. Movement Tick Info
float tickDur = GameManager::instance().getTickDuration();
float curTick = GameManager::instance().getTickTimer();
float progress = curTick / tickDur;
@@ -471,16 +516,40 @@ void QSceneRenderer::drawTickBar(QPainter &painter)
painter.drawText(QRect(barRect.left() + 10, barRect.top() + 4, 120, 14), Qt::AlignLeft | Qt::AlignVCenter, tickLabel);
// Progress bar
QRect progBg(barRect.left() + 10, barRect.top() + 20, 140, 6);
QRect progBg(barRect.left() + 10, barRect.top() + 20, 130, 6);
painter.setBrush(QColor(30, 40, 55));
painter.setPen(Qt::NoPen);
painter.drawRoundedRect(progBg, 3, 3);
QRect progFg(barRect.left() + 10, barRect.top() + 20, (int)(140 * progress), 6);
QRect progFg(barRect.left() + 10, barRect.top() + 20, (int)(130 * progress), 6);
painter.setBrush(QColor(60, 180, 255));
painter.drawRoundedRect(progFg, 3, 3);
// Sim Speed Buttons [1x] [2x] [5x]
// 2. Player 1 National Resources (Empire-wide)
const Player *p1 = GameManager::instance().players().getPlayer(1);
if (p1) {
struct ResItem { QString icon; int val; QColor col; };
std::vector<ResItem> natRes = {
{"images/resources/cultuur.png", p1->culture, QColor(215, 110, 255)},
{"images/resources/kennis.png", p1->science, QColor(60, 190, 255)},
{"images/resources/goud.png", p1->gold, QColor(255, 210, 50)},
{"images/resources/geloof.png", p1->faith, QColor(255, 230, 80)}
};
int startX = barRect.left() + 155;
int itemW = 86;
for (size_t i = 0; i < natRes.size(); ++i) {
int ix = startX + (int)i * itemW;
QImage icon = getTintedIcon(natRes[i].icon, natRes[i].col);
painter.drawImage(QRect(ix, barRect.top() + 9, 16, 16), icon);
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
painter.setPen(natRes[i].col);
painter.drawText(QRect(ix + 20, barRect.top() + 8, itemW - 22, 18), Qt::AlignLeft | Qt::AlignVCenter, QString("%L1").arg(natRes[i].val));
}
}
// 3. Sim Speed Buttons [1x] [2x] [5x]
float curSpeed = GameManager::instance().getSimSpeed();
m_speed1xRect = QRect(barRect.right() - 150, barRect.top() + 6, 42, 22);
m_speed2xRect = QRect(barRect.right() - 102, barRect.top() + 6, 42, 22);
@@ -534,9 +603,18 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
// 2. Draw Units: Teardrop (Druppel in secondary color) + Icon (in primary color)
const float R = 14.0f; // Head circle radius
City *activeCityView = GameManager::instance().getActiveCityView();
int maxCityRadius = TerrainYieldManager::instance().getCityConfig().maxWorkableRadius;
for (const auto &unit : GameManager::instance().getUnits()) {
if (unit->isDead()) continue;
if (activeCityView) {
if (Pathfinder::hexDistance(activeCityView->tileX, activeCityView->tileY, unit->tileX, unit->tileY) > maxCityRadius) {
continue;
}
}
bool inFront = false;
QVector3D topCylinder = unit->worldPos + QVector3D(0.0f, 0.0f, 0.38f);
QPointF tipPos = UnitRenderer::projectToScreen(topCylinder, Projection, vMatrix, width(), height(), inFront);
@@ -589,6 +667,19 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
painter.setBrush(hpFrac > 0.5f ? QColor(70, 220, 90) : (hpFrac > 0.25f ? QColor(255, 190, 40) : QColor(240, 50, 50)));
painter.drawRoundedRect(hpFg, 1.5, 1.5);
// Building progress bar if unit is constructing
if (unit->isBuilding()) {
QRectF bpBg(headCenter.x() - 14.0f, headCenter.y() - R - 1.5f, 28.0f, 3.5f);
painter.setBrush(QColor(30, 30, 30, 220));
painter.setPen(Qt::NoPen);
painter.drawRoundedRect(bpBg, 1.5, 1.5);
float bpFrac = std::max(0.0f, std::min(1.0f, unit->buildProgress));
QRectF bpFg(headCenter.x() - 14.0f, headCenter.y() - R - 1.5f, 28.0f * bpFrac, 3.5f);
painter.setBrush(QColor(255, 185, 45));
painter.drawRoundedRect(bpFg, 1.5, 1.5);
}
// If unit is selected: pulsing selection halo & floating action bubbles
if (unit.get() == selectedUnit) {
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.008f) + 1.0f) * 0.5f;
@@ -624,6 +715,15 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity);
iconFile = "images/actions/build_city.png";
tip = "Steden bouwen (B)";
} else if (act == "boerderij_bouwen" || act == "build_farm") {
isActive = unit->isBuilding();
iconFile = "images/actions/farm.png";
if (unit->isBuilding()) {
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
tip = QString("Aan het bouwen... %1%").arg(pct);
} else {
tip = "Boerderij bouwen (F)";
}
}
// Draw action bubble
@@ -822,6 +922,17 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
if (unit->config.isMilitary()) {
QString combatInfo = QString("Aanval: %1 | Bereik: %2 hex").arg(unit->config.attackDamage).arg(unit->config.attackRange);
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, combatInfo);
} else if (unit->isBuilding()) {
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
QString buildInfo = QString("Aan het bouwen... %1%").arg(pct);
if (!unit->config.infiniteCharges && unit->buildCharges > 0) {
buildInfo += QString(" | Ladingen: %1").arg(unit->buildCharges);
}
painter.setPen(QColor(255, 210, 80));
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, buildInfo);
} else if (unit->config.canBuildFarm() && !unit->config.infiniteCharges && unit->buildCharges > 0) {
QString builderInfo = QString("Civiele eenheid | Ladingen: %1").arg(unit->buildCharges);
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, builderInfo);
} else {
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, "Civiele eenheid");
}
@@ -860,6 +971,17 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
iconFile = "images/actions/build_city.png";
label = "Sticht [B]";
tip = "Steden bouwen (B)";
} else if (act == "boerderij_bouwen" || act == "build_farm") {
isActive = unit->isBuilding();
iconFile = "images/actions/farm.png";
if (unit->isBuilding()) {
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
label = QString("Bouwt %1%").arg(pct);
tip = QString("Bezig met bouwen... %1%").arg(pct);
} else {
label = "Boerderij [F]";
tip = "Boerderij bouwen (F)";
}
}
// Circular bubble button
@@ -883,8 +1005,11 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
void QSceneRenderer::drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix)
{
painter.setRenderHint(QPainter::Antialiasing, true);
City *activeCity = GameManager::instance().getActiveCityView();
for (const auto &city : GameManager::instance().getCities()) {
if (activeCity && city.id != activeCity->id) continue;
const Player *player = GameManager::instance().players().getPlayer(city.playerId);
QColor ribbonColor = player ? player->primaryColor : QColor(255, 215, 60);
@@ -928,9 +1053,47 @@ void QSceneRenderer::drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatri
}
}
void QSceneRenderer::drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
{
painter.setRenderHint(QPainter::Antialiasing, true);
const auto &improvements = GameManager::instance().getTileImprovements();
if (improvements.empty()) return;
for (const auto &pair : improvements) {
const TileImprovement &imp = pair.second;
bool inFront = false;
QPointF screenP = UnitRenderer::projectToScreen(imp.worldPos + QVector3D(0, 0, 0.20f), Projection, vMatrix, width(), height(), inFront);
if (!inFront) continue;
// Draw styled badge for improvement (Farm)
float r = 14.0f;
QRect badgeRect((int)(screenP.x() - r), (int)(screenP.y() - r), (int)(r * 2), (int)(r * 2));
// Background disc
painter.setBrush(QColor(24, 30, 20, 235));
painter.setPen(QPen(QColor(235, 195, 60), 1.6));
painter.drawEllipse(badgeRect);
// Icon inside disc
QImage farmIcon = getTintedIcon("images/actions/farm.png", QColor(255, 220, 75));
painter.drawImage(QRectF(screenP.x() - 8, screenP.y() - 8, 16, 16), farmIcon);
// Label pill under badge
QRect labelRect((int)screenP.x() - 26, (int)screenP.y() + 13, 52, 13);
painter.setBrush(QColor(15, 20, 28, 220));
painter.setPen(QPen(QColor(200, 165, 50, 150), 1.0));
painter.drawRoundedRect(labelRect, 3, 3);
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
painter.setPen(QColor(255, 230, 130));
painter.drawText(labelRect, Qt::AlignCenter, "Boerderij");
}
}
void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
{
m_citySlotHits.clear();
m_cityBuyTileHits.clear();
City *city = GameManager::instance().getActiveCityView();
if (!city) return;
@@ -943,8 +1106,8 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
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) {
for (int dx = -maxRadius - 2; dx <= maxRadius + 2; ++dx) {
for (int dy = -maxRadius - 2; dy <= maxRadius + 2; ++dy) {
int hx = city->tileX + dx;
int hy = city->tileY + dy;
if (hx < 0 || hx >= MAP_WIDTH || hy < 0 || hy >= MAP_HEIGHT) continue;
@@ -996,6 +1159,13 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
bool isWorked = city->isTileWorked(hx, hy);
QString res = isWorked ? city->getWorkedResource(hx, hy) : yCfg->primaryResource;
int amount = yCfg->getYield(res);
const TileImprovement *imp = GameManager::instance().getImprovementAt(hx, hy);
if (imp) {
const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId);
if (iCfg && iCfg->yieldBonus.contains(res)) {
amount += iCfg->yieldBonus.value(res, 0);
}
}
float slotR = 15.0f;
QRect slotRect((int)(sCenter.x() - slotR), (int)(sCenter.y() - slotR), (int)(slotR * 2), (int)(slotR * 2));
@@ -1010,6 +1180,12 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
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);
// If tile has farm/improvement, draw small icon on upper right of slot
if (imp) {
QImage fIcon = getTintedIcon("images/actions/farm.png", QColor(255, 215, 60));
painter.drawImage(QRectF(sCenter.x() + 8, sCenter.y() - 16, 14, 14), fIcon);
}
// Yield pill below slot
QRect pillRect((int)sCenter.x() - 26, (int)sCenter.y() + 16, 52, 14);
painter.setBrush(QColor(15, 20, 28, 220));
@@ -1029,33 +1205,64 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
}
// 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));
int otherCityId = -1;
bool isOwnedByOther = GameManager::instance().isTileOwned(hx, hy, &otherCityId);
if (!isOwnedByOther) {
// Unowned tile: Can be bought with culture!
int cultureCost = TerrainYieldManager::instance().getCityConfig().tileCultureCost;
bool canAfford = (player && player->culture >= cultureCost);
QRect buyRect((int)sCenter.x() - 34, (int)sCenter.y() - 14, 68, 28);
m_cityBuyTileHits.push_back({buyRect, hx, hy});
// Background & border
painter.setBrush(canAfford ? QColor(40, 20, 55, 235) : QColor(24, 18, 30, 215));
painter.setPen(QPen(canAfford ? QColor(220, 110, 255) : QColor(120, 80, 140), canAfford ? 1.8 : 1.0));
painter.drawRoundedRect(buyRect, 5, 5);
// Culture icon
QImage cultIcon = getTintedIcon("images/resources/cultuur.png", canAfford ? QColor(225, 120, 255) : QColor(150, 110, 170));
painter.drawImage(QRect(buyRect.left() + 4, buyRect.top() + 6, 16, 16), cultIcon);
// Price text
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
painter.setPen(canAfford ? QColor(255, 230, 255) : QColor(165, 145, 180));
QString costStr = QString("%L1").arg(cultureCost);
painter.drawText(QRect(buyRect.left() + 22, buyRect.top() + 2, buyRect.width() - 24, 13), Qt::AlignLeft | Qt::AlignVCenter, costStr);
// "Koop" subtitle
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
painter.setPen(canAfford ? QColor(220, 140, 255) : QColor(140, 120, 155));
painter.drawText(QRect(buyRect.left() + 22, buyRect.top() + 14, buyRect.width() - 24, 12), Qt::AlignLeft | Qt::AlignVCenter, "KOOP");
} else {
// Owned by another city
QRect lockRect((int)sCenter.x() - 16, (int)sCenter.y() - 8, 32, 16);
painter.setBrush(QColor(40, 20, 20, 190));
painter.setPen(QPen(QColor(160, 60, 60, 140), 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");
painter.setPen(QColor(230, 130, 130));
painter.drawText(lockRect, Qt::AlignCenter, "BEZET");
}
}
}
}
// 2. City Management Top Card
int cardW = 500, cardH = 145;
QRect cardRect((width() - cardW) / 2, 16, cardW, cardH);
int cardW = 560, cardH = 174;
QRect cardRect((width() - cardW) / 2, 14, cardW, cardH);
painter.setBrush(QColor(16, 22, 32, 242));
painter.setBrush(QColor(16, 22, 32, 245));
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");
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 8, 300, 24), Qt::AlignLeft | Qt::AlignVCenter, city->name.toUpper() + " - STADSBEHEER");
// Close button [X]
m_closeCityViewBtnRect = QRect(cardRect.right() - 88, cardRect.top() + 10, 74, 24);
m_closeCityViewBtnRect = QRect(cardRect.right() - 88, cardRect.top() + 8, 74, 24);
painter.setBrush(QColor(40, 50, 68, 220));
painter.setPen(QPen(secondaryCol, 1.2));
painter.drawRoundedRect(m_closeCityViewBtnRect, 4, 4);
@@ -1063,22 +1270,34 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
painter.setPen(QColor(220, 235, 255));
painter.drawText(m_closeCityViewBtnRect, Qt::AlignCenter, "[X] Sluiten");
// Population Stats Row
// Row 1: Population & Local Resources (Food & Labor are local to the city)
int totalPop = city->getTotalPop();
int assigned = city->getAssignedPop();
int avail = city->getAvailablePop();
painter.setFont(QFont("sans-serif", 9));
painter.setFont(QFont("sans-serif", 8));
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);
QString popInfo = QString("Lokaal: Inwoners: %L1 (%2 Pop: %3 aan het werk, %4 vrij) | Arbeid: %L5")
.arg(city->inhabitants).arg(totalPop).arg(assigned).arg(avail).arg(city->accumulatedLabor);
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 34, cardRect.width() - 32, 16), Qt::AlignLeft | Qt::AlignVCenter, popInfo);
// Row 2: National Resources (Culture, Science, Gold, Faith belong to the nation)
int cult = player ? player->culture : 0;
int sci = player ? player->science : 0;
int gld = player ? player->gold : 0;
int fth = player ? player->faith : 0;
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
painter.setPen(QColor(220, 160, 255));
QString natInfo = QString("Nationaal: Cultuur: %L1 | Kennis: %L2 | Goud: %L3 | Geloof: %L4")
.arg(cult).arg(sci).arg(gld).arg(fth);
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 52, cardRect.width() - 32, 16), Qt::AlignLeft | Qt::AlignVCenter, natInfo);
// 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);
painter.drawLine(cardRect.left() + 16, cardRect.top() + 72, cardRect.right() - 16, cardRect.top() + 72);
// Total Yields Summary Row
// Row 3: Total Yields Summary Row (Base 500 + worked tiles)
CityYieldSummary summary = GameManager::instance().calculateCityYields(*city, m_point, m_numPoints05);
struct YieldItem { QString name; int val; QString icon; QColor col; };
@@ -1094,29 +1313,29 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
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);
QRect yRect(cx, cardRect.top() + 78, colW - 6, 42);
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.drawImage(QRect(yRect.left() + 6, yRect.top() + 5, 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.drawText(QRect(yRect.left() + 26, yRect.top() + 5, yRect.width() - 28, 16), Qt::AlignLeft | Qt::AlignVCenter, yields[i].name);
painter.setFont(QFont("sans-serif", 9, QFont::Bold));
painter.setFont(QFont("sans-serif", 8, 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));
painter.drawText(QRect(yRect.left() + 6, yRect.top() + 22, yRect.width() - 12, 16), Qt::AlignLeft | Qt::AlignVCenter, QString("+%1/tick").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.");
painter.drawText(QRect(cardRect.left() + 16, cardRect.bottom() - 18, cardRect.width() - 32, 16), Qt::AlignCenter,
"Klik op een tegel om 1 Pop toe te wijzen | Klik op een vrije tegel om deze te kopen met Cultuur.");
}
void QSceneRenderer::mousePressEvent(QMouseEvent *event)
@@ -1124,6 +1343,9 @@ 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())) {
cameraPosition = m_savedCamPos;
m_zoom = m_savedZoom;
zRot = m_savedZRot;
GameManager::instance().exitCityView();
update();
return;
@@ -1151,17 +1373,28 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
}
}
}
// 2. Buy tile clicks
for (const auto &buyHit : m_cityBuyTileHits) {
if (buyHit.rect.contains(event->pos())) {
City *city = GameManager::instance().getActiveCityView();
if (city) {
GameManager::instance().buyTileForCity(city->id, buyHit.tx, buyHit.ty);
update();
return;
}
}
}
return; // In City View, ignore all other clicks
}
// Check if clicked on a City Banner to open City View
for (const auto &cb : m_cityBannerHits) {
if (cb.rect.contains(event->pos())) {
m_savedCamPos = cameraPosition;
m_savedZoom = m_zoom;
m_savedZRot = zRot;
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;
}
@@ -1229,6 +1462,8 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
}
} else if (ab.actionId == "steden_bouwen" || ab.actionId == "build_city") {
GameManager::instance().commandBuildCitySelected(m_point, m_numPoints05);
} else if (ab.actionId == "boerderij_bouwen" || ab.actionId == "build_farm") {
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
}
update();
return;
@@ -1238,11 +1473,7 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
// Compute view matrix for raycasting and 3D screen tests
QMatrix4x4 vMatrix;
cameraUpDirection = cameraTransformation * QVector3D(0, 0, 1);
QVector3D lookat = QVector3D(
cameraPosition.x() + m_zoom * m_zoom / 5.0f * cos(PI * zRot / 180.0f),
cameraPosition.y() + m_zoom * m_zoom / 5.0f * sin(PI * zRot / 180.0f),
0
);
QVector3D lookat = getCameraLookAt();
vMatrix.lookAt(cameraPosition, lookat, cameraUpDirection);
// 5. Left Click: Check if directly clicked on any unit's Druppel pin
@@ -1304,9 +1535,10 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
} else {
City *cityOnTile = GameManager::instance().getCityAt(hitX, hitY);
if (cityOnTile) {
m_savedCamPos = cameraPosition;
m_savedZoom = m_zoom;
m_savedZRot = zRot;
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();
}
@@ -1330,6 +1562,12 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
{
m_mouseCurrentPos = event->pos();
// Disable panning, rotation and minimap drag in City View
if (GameManager::instance().isInCityView()) {
update();
return;
}
QRect mapRect = getMinimapRect();
if (m_draggingMinimap && (event->buttons() & Qt::LeftButton)) {
QVector2D wPos = minimapToWorld(event->pos(), mapRect);
@@ -1357,6 +1595,18 @@ void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event)
void QSceneRenderer::keyPressEvent(QKeyEvent *e)
{
// In City View: disable WASD panning and Q/E rotation; only allow Escape to exit
if (GameManager::instance().isInCityView()) {
if (e->key() == Qt::Key_Escape) {
cameraPosition = m_savedCamPos;
m_zoom = m_savedZoom;
zRot = m_savedZRot;
GameManager::instance().exitCityView();
update();
}
return;
}
float speed = (e->modifiers() & Qt::ShiftModifier) ? 1.2f : 0.4f;
// Movement & camera controls
@@ -1400,10 +1650,11 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e)
else if (e->key() == Qt::Key_B && sel && sel->config.canBuildCity()) {
GameManager::instance().commandBuildCitySelected(m_point, m_numPoints05);
}
else if (e->key() == Qt::Key_F && sel && sel->config.canBuildFarm()) {
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
}
else if (e->key() == Qt::Key_Escape) {
if (GameManager::instance().isInCityView()) {
GameManager::instance().exitCityView();
} else if (GameManager::instance().currentActionMode() != ActionMode::None) {
if (GameManager::instance().currentActionMode() != ActionMode::None) {
GameManager::instance().setActionMode(ActionMode::None);
} else {
GameManager::instance().deselectUnit();
@@ -1425,6 +1676,11 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e)
void QSceneRenderer::wheelEvent(QWheelEvent *e)
{
// Disable zooming in City View
if (GameManager::instance().isInCityView()) {
return;
}
QRect card = getMinimapCardRect();
if (card.contains(e->pos()) || getZoomOutButtonRect().contains(e->pos()) || getZoomInButtonRect().contains(e->pos())) {
if (e->delta() > 0) {
@@ -1437,8 +1693,8 @@ void QSceneRenderer::wheelEvent(QWheelEvent *e)
}
m_zoom -= e->delta() / 100.0f;
if (m_zoom < 2.0f) m_zoom = 2.0f;
if (m_zoom > 16.0f) m_zoom = 16.0f;
if (m_zoom < m_cameraMinZoom) m_zoom = m_cameraMinZoom;
if (m_zoom > m_cameraMaxZoom) m_zoom = m_cameraMaxZoom;
update();
}
@@ -1462,17 +1718,38 @@ void QSceneRenderer::paintGL()
QMatrix4x4 cameraTrans;
QMatrix4x4 vMatrix;
City *cityView = GameManager::instance().getActiveCityView();
int cityViewRadius = cityView ? TerrainYieldManager::instance().getCityConfig().maxWorkableRadius : -1;
cameraTrans.rotate(zRot, 0, 0, 1);
cameraUpDirection = cameraTrans * QVector3D(0, 0, 1);
cameraPosition.setZ(m_zoom);
QVector3D lookat = QVector3D(
cameraPosition.x() + m_zoom * m_zoom / 5.0f * cos(PI * zRot / 180.0f),
cameraPosition.y() + m_zoom * m_zoom / 5.0f * sin(PI * zRot / 180.0f),
0.0f
QVector3D eyePos;
QVector3D lookat;
if (cityView) {
float effectiveZoom = TerrainYieldManager::instance().getCityConfig().cityViewZoom;
float pitchDist = effectiveZoom * effectiveZoom / 5.0f;
float rad = zRot * PI / 180.0f;
lookat = cityView->worldPos;
eyePos = QVector3D(
lookat.x() - pitchDist * cos(rad),
lookat.y() - pitchDist * sin(rad),
effectiveZoom
);
vMatrix.lookAt(cameraPosition, lookat, cameraUpDirection);
cameraPosition = eyePos;
} else {
cameraPosition.setZ(m_zoom);
eyePos = cameraPosition;
lookat = getCameraLookAt();
}
vMatrix.lookAt(eyePos, lookat, cameraUpDirection);
if (cityView) {
glClearColor(0.06f, 0.08f, 0.12f, 1.0f);
} else {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Update simulation
@@ -1482,12 +1759,13 @@ void QSceneRenderer::paintGL()
GameManager::instance().update(deltaTime, m_point, m_numPoints05);
// Visible terrain tiles bounding box
float minWorldX = std::min(cameraPosition.x(), lookat.x());
float maxWorldX = std::max(cameraPosition.x(), lookat.x());
float minWorldY = std::min(cameraPosition.y(), lookat.y());
float maxWorldY = std::max(cameraPosition.y(), lookat.y());
float minWorldX = std::min(eyePos.x(), lookat.x());
float maxWorldX = std::max(eyePos.x(), lookat.x());
float minWorldY = std::min(eyePos.y(), lookat.y());
float maxWorldY = std::max(eyePos.y(), lookat.y());
float margin = std::max(m_zoom * 3.0f + 15.0f, 35.0f);
// Margin around visible focal area (provides ample buffer for mountains and rotating view)
float margin = cityView ? 20.0f : std::max(10.0f, m_zoom * 1.4f + 4.0f);
minWorldX -= margin;
maxWorldX += margin;
minWorldY -= margin;
@@ -1518,10 +1796,13 @@ void QSceneRenderer::paintGL()
}
}
// 1. Render terrain tiles
// 1. Render terrain tiles (in city view: ONLY render tiles within cityViewRadius!)
for (int y = minTileY; y <= maxTileY; ++y) {
for (int x = minTileX; x <= maxTileX; ++x) {
tiles->operator()(x, y).paint(Projection, cameraPosition, cameraUpDirection, vMatrix, currentTime);
if (cityView && Pathfinder::hexDistance(cityView->tileX, cityView->tileY, x, y) > cityViewRadius) {
continue;
}
tiles->operator()(x, y).paint(Projection, eyePos, cameraUpDirection, vMatrix, currentTime);
}
}
@@ -1538,22 +1819,29 @@ void QSceneRenderer::paintGL()
Projection,
vMatrix,
vSunPos,
selected ? selected->id : -1
selected ? selected->id : -1,
cityViewRadius,
cityView ? cityView->tileX : -1,
cityView ? cityView->tileY : -1
);
// 3. Render 2D UI & HUD Overlays via QPainter
QPainter painter(this);
drawCityBorders(painter, vMatrix);
drawImprovementsOverlay(painter, vMatrix);
if (!cityView) {
drawPathAndRange(painter, selected, vMatrix);
drawMinimap(painter);
drawTickBar(painter);
drawUnitsOverlay(painter, vMatrix);
drawFloatingTexts(painter, vMatrix);
if (GameManager::instance().isInCityView()) {
drawCityViewOverlay(painter, vMatrix);
} else if (selected) {
if (selected) {
drawUnitCard(painter, selected);
}
} else {
drawUnitsOverlay(painter, vMatrix);
drawFloatingTexts(painter, vMatrix);
drawCityViewOverlay(painter, vMatrix);
}
painter.end();
}
+21
View File
@@ -11,6 +11,11 @@ uniform sampler2D texTundra; // Unit 8 (Texturelabs_Soil_135XL tundra soil)
uniform sampler2D texRedSand; // Unit 9 (Texturelabs_RedSand prairie sand)
uniform sampler2D texRedRock; // Unit 10 (Texturelabs_RedRock canyon cliff)
uniform sampler2D texForest; // Unit 11 (Texturelabs_Forest_seamless woodland floor)
uniform sampler2D texFarm; // Unit 12 (images/farm.jpeg)
uniform int isFarm;
uniform float farmTextureScale;
uniform int farmMappingMode;
uniform float time;
uniform int riverMask;
@@ -213,6 +218,22 @@ void main() {
+ tundraColor * tundraWeight
+ redSandColor * prairieWeight;
// Farm improvement texture applied when a farm is present
if (isFarm == 1) {
float scale = max(0.01, farmTextureScale);
vec2 farmUV;
if (farmMappingMode == 1) {
// Hex-centered mapping with aspect ratio correction
vec2 centered = vec2(tilePos.x - 0.5, (tilePos.y - 0.4328799) * 1.154734411);
farmUV = centered * scale + vec2(0.5, 0.5);
} else {
// World-space coordinates: perfectly seamless and continuous across adjacent hex tiles
farmUV = vWorldPos.xy * scale;
}
vec4 farmColor = texture(texFarm, farmUV);
biomeGround = farmColor;
}
// Shoreline beach: where gentle slopes meet water (sea level up to ~110m, or along rivers)
// Steep slopes (norm.z < 0.55) remain rocky cliffs; gentle slopes (norm.z > 0.80) get a sandy beach
float gentleSlope = smoothstep(0.55, 0.85, norm.z);
+67 -36
View File
@@ -1,4 +1,5 @@
#include "terrainyieldmanager.h"
#include "terrainmovementmanager.h"
#include "yamlparser.h"
#include "pathfinder.h"
#include <QDebug>
@@ -17,6 +18,9 @@ TerrainYieldManager::TerrainYieldManager()
bool TerrainYieldManager::loadConfigs(const QString &baseDir)
{
// 0. Load terrain_movement.yaml
TerrainMovementManager::instance().loadConfig(baseDir + "/config/terrain_movement.yaml");
// 1. Load city.yaml
QString cityFile = baseDir + "/config/city.yaml";
YamlNode cityRoot = YamlParser::parseFile(cityFile);
@@ -26,6 +30,25 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir)
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);
m_cityConfig.cityViewZoom = cityRoot["city_view_zoom"].asFloat(4.8f);
m_cityConfig.baseResourceYieldPerTick = cityRoot["base_resource_yield_per_tick"].asInt(500);
m_cityConfig.tileCultureCost = cityRoot["tile_culture_cost"].asInt(10000);
m_cityConfig.foodToInhabitantsRatio = cityRoot["food_to_inhabitants_ratio"].asFloat(1.0f);
if (cityRoot.hasChild("base_yields")) {
const YamlNode &bNode = cityRoot["base_yields"];
for (auto it = bNode.children.begin(); it != bNode.children.end(); ++it) {
m_cityConfig.baseYields[it.key()] = it.value().asInt(m_cityConfig.baseResourceYieldPerTick);
}
}
if (cityRoot.hasChild("initial_national_resources")) {
const YamlNode &rNode = cityRoot["initial_national_resources"];
for (auto it = rNode.children.begin(); it != rNode.children.end(); ++it) {
m_cityConfig.initialNationalResources[it.key()] = it.value().asInt(0);
}
}
}
// 2. Load terrain_yields.yaml
@@ -53,48 +76,46 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir)
m_yieldConfigs[typeKey] = cfg;
}
// 3. Load improvements.yaml
QString impFile = baseDir + "/config/improvements.yaml";
YamlNode impRoot = YamlParser::parseFile(impFile);
for (auto it = impRoot.children.begin(); it != impRoot.children.end(); ++it) {
QString impKey = it.key();
const YamlNode &node = it.value();
ImprovementConfig cfg;
cfg.id = node.hasChild("id") ? node["id"].asString() : impKey;
cfg.name = node.hasChild("name") ? node["name"].asString() : cfg.id;
cfg.action = node.hasChild("action") ? node["action"].asString() : "boerderij_bouwen";
cfg.texturePath = node.hasChild("texture") ? node["texture"].asString() : "images/farm.jpeg";
cfg.textureScale = node.hasChild("texture_scale") ? node["texture_scale"].asFloat(0.6f) : 0.6f;
cfg.textureMapping = node.hasChild("texture_mapping") ? node["texture_mapping"].asString("world") : "world";
if (node.hasChild("valid_terrains")) {
cfg.validTerrains = node["valid_terrains"].listValues;
} else {
cfg.validTerrains = QStringList{"grasland", "tundra"};
}
if (node.hasChild("yield_bonus")) {
const YamlNode &ybNode = node["yield_bonus"];
for (auto ybIt = ybNode.children.begin(); ybIt != ybNode.children.end(); ++ybIt) {
cfg.yieldBonus[ybIt.key()] = ybIt.value().asInt(0);
}
}
m_improvementConfigs[impKey] = cfg;
}
m_loaded = true;
qDebug() << "TerrainYieldManager loaded" << m_yieldConfigs.size() << "terrain types and city config.";
qDebug() << "TerrainYieldManager loaded" << m_yieldConfigs.size() << "terrain types,"
<< m_improvementConfigs.size() << "improvements, 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";
}
return TerrainMovementManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
}
const TerrainYieldConfig* TerrainYieldManager::getYieldConfig(const QString &terrainType) const
@@ -105,3 +126,13 @@ const TerrainYieldConfig* TerrainYieldManager::getYieldConfig(const QString &ter
}
return nullptr;
}
const ImprovementConfig* TerrainYieldManager::getImprovementConfig(const QString &id) const
{
auto it = m_improvementConfigs.find(id);
if (it != m_improvementConfigs.end()) {
return &it.value();
}
return nullptr;
}
+47 -1
View File
@@ -29,10 +29,52 @@ struct CityConfig {
int initialBorderRadius;
int maxWorkableRadius;
int defaultTileSlots;
float cityViewZoom;
int baseResourceYieldPerTick;
int tileCultureCost;
float foodToInhabitantsRatio;
QMap<QString, int> baseYields;
QMap<QString, int> initialNationalResources;
CityConfig()
: initialInhabitants(1000), inhabitantsPerPop(100000),
initialBorderRadius(1), maxWorkableRadius(3), defaultTileSlots(1) {}
initialBorderRadius(1), maxWorkableRadius(3), defaultTileSlots(1),
cityViewZoom(4.8f),
baseResourceYieldPerTick(500), tileCultureCost(10000),
foodToInhabitantsRatio(1.0f)
{
baseYields["voedsel"] = 500;
baseYields["arbeid"] = 500;
baseYields["kennis"] = 500;
baseYields["cultuur"] = 500;
baseYields["geloof"] = 500;
baseYields["goud"] = 500;
initialNationalResources["cultuur"] = 10000;
initialNationalResources["kennis"] = 0;
initialNationalResources["geloof"] = 0;
initialNationalResources["goud"] = 0;
}
int getBaseYield(const QString &res) const {
return baseYields.value(res, baseResourceYieldPerTick);
}
};
struct ImprovementConfig {
QString id;
QString name;
QString action;
QString texturePath;
float textureScale;
QString textureMapping; // "world" or "hex"
QStringList validTerrains;
QMap<QString, int> yieldBonus; // e.g. "voedsel": 1000
ImprovementConfig()
: id(""), name(""), action(""), texturePath("images/farm.jpeg"), textureScale(0.6f), textureMapping("world") {}
};
class TerrainYieldManager {
@@ -45,11 +87,15 @@ public:
const TerrainYieldConfig* getYieldConfig(const QString &terrainType) const;
const CityConfig& getCityConfig() const { return m_cityConfig; }
const ImprovementConfig* getImprovementConfig(const QString &id) const;
const QMap<QString, ImprovementConfig>& getAllImprovementConfigs() const { return m_improvementConfigs; }
private:
TerrainYieldManager();
~TerrainYieldManager() = default;
QMap<QString, TerrainYieldConfig> m_yieldConfigs;
QMap<QString, ImprovementConfig> m_improvementConfigs;
CityConfig m_cityConfig;
bool m_loaded;
};
+27
View File
@@ -2,6 +2,7 @@
#include "matrix2d.h"
#include <math.h>
#include "qscenerenderer.h"
#include "gamemanager.h"
#include <QVector3D>
QImage * Tile::m_image_grass = NULL;
QOpenGLTexture * Tile::m_texture_grass = NULL;
@@ -27,6 +28,8 @@ QOpenGLTexture * Tile::m_texture_redrock = NULL;
QImage * Tile::m_image_redrock = NULL;
QOpenGLTexture * Tile::m_texture_forest = NULL;
QImage * Tile::m_image_forest = NULL;
QOpenGLTexture * Tile::m_texture_farm = NULL;
QImage * Tile::m_image_farm = NULL;
Vertex * Tile::m_vertices= NULL;
int Tile::m_numVertex;
@@ -147,6 +150,8 @@ void Tile::init(QSceneRenderer* s, int x, int y)
Tile::m_image_redrock = new QImage("/home/bart/project/tiletest/images/Texturelabs_RedRock_seamless.jpg");
if (Tile::m_image_forest == NULL)
Tile::m_image_forest = new QImage("/home/bart/project/tiletest/images/Texturelabs_Forest_seamless.jpg");
if (Tile::m_image_farm == NULL)
Tile::m_image_farm = new QImage("/home/bart/project/tiletest/images/farm.jpeg");
if (Tile::m_texture_grass == NULL) {
m_texture_grass = new QOpenGLTexture(*m_image_grass);
@@ -232,6 +237,13 @@ void Tile::init(QSceneRenderer* s, int x, int y)
m_texture_forest->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_forest->generateMipMaps();
}
if (Tile::m_texture_farm == NULL && Tile::m_image_farm != NULL) {
m_texture_farm = new QOpenGLTexture(*m_image_farm);
m_texture_farm->setWrapMode(QOpenGLTexture::Repeat);
m_texture_farm->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
m_texture_farm->setMagnificationFilter(QOpenGLTexture::Linear);
m_texture_farm->generateMipMaps();
}
if (first){
m_program = new QOpenGLShaderProgram();
@@ -302,6 +314,9 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
m_texture_redsand->bind(9);
m_texture_redrock->bind(10);
m_texture_forest->bind(11);
if (m_texture_farm) {
m_texture_farm->bind(12);
}
// Render using our shader
m_program->bind();
@@ -334,6 +349,18 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
m_program->setUniformValue("texRedSand", 9);
m_program->setUniformValue("texRedRock", 10);
m_program->setUniformValue("texForest", 11);
m_program->setUniformValue("texFarm", 12);
const TileImprovement *imp = GameManager::instance().getImprovementAt(m_x, m_y);
bool isFarm = (imp != nullptr && imp->improvementId == "farm");
m_program->setUniformValue("isFarm", isFarm ? 1 : 0);
const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig("farm");
float fScale = farmCfg ? farmCfg->textureScale : 0.6f;
int mappingMode = (farmCfg && farmCfg->textureMapping == "hex") ? 1 : 0;
m_program->setUniformValue("farmTextureScale", fScale);
m_program->setUniformValue("farmMappingMode", mappingMode);
m_program->setUniformValue("time", time);
m_program->setUniformValue("riverMask", (int)m_riverMask);
m_object.bind();
+26
View File
@@ -44,8 +44,23 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
cfg.cost = root.hasChild("cost") ? root["cost"].asInt(20) : 20;
cfg.attackDamage = root.hasChild("attack_damage") ? root["attack_damage"].asInt(0) : 0;
cfg.attackRange = root.hasChild("attack_range") ? root["attack_range"].asInt(1) : 1;
cfg.buildCharges = root.hasChild("build_charges") ? root["build_charges"].asInt(-1) : -1;
cfg.infiniteCharges = root.hasChild("infinite_charges") ? root["infinite_charges"].asBool(cfg.buildCharges < 0) : (cfg.buildCharges < 0);
cfg.iconPath = root.hasChild("icon") ? root["icon"].asString() : "";
if (root.hasChild("domain")) {
cfg.movementDomain = stringToMovementDomain(root["domain"].asString());
} else if (root.hasChild("movement_domain")) {
cfg.movementDomain = stringToMovementDomain(root["movement_domain"].asString());
}
if (root.hasChild("impassable_terrains")) {
cfg.impassableTerrains = root["impassable_terrains"].listValues;
}
if (root.hasChild("passable_terrains")) {
cfg.passableTerrains = root["passable_terrains"].listValues;
}
if (root.hasChild("actions")) {
cfg.actions = root["actions"].listValues;
}
@@ -54,6 +69,17 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
cfg.actions.prepend("verplaatsen");
}
auto parseActionMap = [&](const QString &key) {
if (root.hasChild(key)) {
const YamlNode &sNode = root[key];
for (auto it = sNode.children.begin(); it != sNode.children.end(); ++it) {
cfg.actionSpeeds[it.key()] = it.value().asFloat(10.0f);
}
}
};
parseActionMap("action_speeds");
parseActionMap("action_durations");
m_configs[cfg.id] = cfg;
return true;
}
+21 -2
View File
@@ -6,6 +6,8 @@
#include <QMap>
#include <vector>
#include "terrainmovementmanager.h"
enum class UnitCategory {
Military, // militair
Reconnaissance, // verkenning
@@ -33,23 +35,40 @@ struct UnitConfig {
QString id;
QString name;
UnitCategory category;
MovementDomain movementDomain;
int health;
float movementSpeed;
int cost;
int attackDamage;
int attackRange; // 1 for melee, >1 for ranged
int buildCharges;
bool infiniteCharges;
QString iconPath;
QStringList actions;
QMap<QString, float> actionSpeeds; // Duration in seconds per action (e.g. "boerderij_bouwen": 10.0f)
QStringList impassableTerrains;
QStringList passableTerrains;
UnitConfig()
: id(""), name(""), category(UnitCategory::Builder), health(100),
: id(""), name(""), category(UnitCategory::Builder),
movementDomain(MovementDomain::Land), health(100),
movementSpeed(2.0f), cost(10), attackDamage(0), attackRange(1),
iconPath("") {}
buildCharges(-1), infiniteCharges(true), iconPath("") {}
bool isRanged() const { return attackRange > 1; }
bool isMilitary() const { return category == UnitCategory::Military; }
bool isNaval() const { return movementDomain == MovementDomain::Naval; }
bool isLand() const { return movementDomain == MovementDomain::Land; }
bool canAttack() const { return actions.contains("aanvallen") || isMilitary(); }
bool canBuildCity() const { return actions.contains("steden_bouwen") || actions.contains("build_city"); }
bool canBuildFarm() const { return actions.contains("boerderij_bouwen") || actions.contains("build_farm"); }
float getActionDuration(const QString &actionName, float fallback = 10.0f) const {
if (actionSpeeds.contains(actionName)) {
return actionSpeeds.value(actionName);
}
return fallback;
}
};
class UnitConfigManager {