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
+367 -79
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));
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();
}