From 17901ce25c9debd93c1ac698b6abc3eba68d7761 Mon Sep 17 00:00:00 2001 From: bart Date: Sun, 8 Nov 2026 17:01:26 +0100 Subject: [PATCH] Refactor farm building speed to builder unit action_speeds and enable infinite charges --- config/improvements.yaml | 15 ++ config/units/builder.yaml | 15 ++ gamemanager.cpp | 244 ++++++++++++++++++++- qscenerenderer.cpp | 446 +++++++++++++++++++++++++++++++------- shaders/simple.frag | 21 ++ terrainyieldmanager.cpp | 103 ++++++--- terrainyieldmanager.h | 48 +++- tile.cpp | 27 +++ unitconfig.cpp | 26 +++ unitconfig.h | 23 +- 10 files changed, 847 insertions(+), 121 deletions(-) create mode 100644 config/improvements.yaml create mode 100644 config/units/builder.yaml diff --git a/config/improvements.yaml b/config/improvements.yaml new file mode 100644 index 0000000..9eed310 --- /dev/null +++ b/config/improvements.yaml @@ -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 diff --git a/config/units/builder.yaml b/config/units/builder.yaml new file mode 100644 index 0000000..541696b --- /dev/null +++ b/config/units/builder.yaml @@ -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 diff --git a/gamemanager.cpp b/gamemanager.cpp index 0d0a70c..dce51be 100644 --- a/gamemanager.cpp +++ b/gamemanager.cpp @@ -2,6 +2,8 @@ #include "pathfinder.h" #include "yamlparser.h" #include "terrainyieldmanager.h" +#include "terrainmovementmanager.h" +#include "localization.h" #include #include @@ -23,6 +25,9 @@ void GameManager::init(const QString &baseDir, Matrix2D *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 *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 *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 *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 *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, Matrix2DgetYield(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 *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 *mapPoints, int numPoints05) { Unit *selected = getSelectedUnit(); @@ -243,13 +366,31 @@ void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2Dconfig.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> 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 *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 *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); +} + diff --git a/qscenerenderer.cpp b/qscenerenderer.cpp index e6f6b1d..6074f82 100644 --- a/qscenerenderer.cpp +++ b/qscenerenderer.cpp @@ -6,6 +6,7 @@ #include #include #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 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)); - 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"); + 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(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 - ); - vMatrix.lookAt(cameraPosition, lookat, cameraUpDirection); + 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 + ); + 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); - drawPathAndRange(painter, selected, vMatrix); - drawMinimap(painter); - drawTickBar(painter); - drawUnitsOverlay(painter, vMatrix); - drawFloatingTexts(painter, vMatrix); - - if (GameManager::instance().isInCityView()) { + drawImprovementsOverlay(painter, vMatrix); + if (!cityView) { + drawPathAndRange(painter, selected, vMatrix); + drawMinimap(painter); + drawTickBar(painter); + drawUnitsOverlay(painter, vMatrix); + drawFloatingTexts(painter, vMatrix); + if (selected) { + drawUnitCard(painter, selected); + } + } else { + drawUnitsOverlay(painter, vMatrix); + drawFloatingTexts(painter, vMatrix); drawCityViewOverlay(painter, vMatrix); - } else if (selected) { - drawUnitCard(painter, selected); } painter.end(); } diff --git a/shaders/simple.frag b/shaders/simple.frag index bdbe0c5..9200858 100644 --- a/shaders/simple.frag +++ b/shaders/simple.frag @@ -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); diff --git a/terrainyieldmanager.cpp b/terrainyieldmanager.cpp index a43c38b..8ee0fa1 100644 --- a/terrainyieldmanager.cpp +++ b/terrainyieldmanager.cpp @@ -1,4 +1,5 @@ #include "terrainyieldmanager.h" +#include "terrainmovementmanager.h" #include "yamlparser.h" #include "pathfinder.h" #include @@ -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 *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; +} + diff --git a/terrainyieldmanager.h b/terrainyieldmanager.h index 59c3580..ad4539e 100644 --- a/terrainyieldmanager.h +++ b/terrainyieldmanager.h @@ -29,10 +29,52 @@ struct CityConfig { int initialBorderRadius; int maxWorkableRadius; int defaultTileSlots; + float cityViewZoom; + + int baseResourceYieldPerTick; + int tileCultureCost; + float foodToInhabitantsRatio; + + QMap baseYields; + QMap 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 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& getAllImprovementConfigs() const { return m_improvementConfigs; } + private: TerrainYieldManager(); ~TerrainYieldManager() = default; QMap m_yieldConfigs; + QMap m_improvementConfigs; CityConfig m_cityConfig; bool m_loaded; }; diff --git a/tile.cpp b/tile.cpp index bddf243..c552d83 100644 --- a/tile.cpp +++ b/tile.cpp @@ -2,6 +2,7 @@ #include "matrix2d.h" #include #include "qscenerenderer.h" +#include "gamemanager.h" #include 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(); diff --git a/unitconfig.cpp b/unitconfig.cpp index 893c3a2..b71bc2a 100644 --- a/unitconfig.cpp +++ b/unitconfig.cpp @@ -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; } diff --git a/unitconfig.h b/unitconfig.h index f82f313..7e3ae19 100644 --- a/unitconfig.h +++ b/unitconfig.h @@ -6,6 +6,8 @@ #include #include +#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 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 {