3711 lines
170 KiB
C++
3711 lines
170 KiB
C++
#include "qscenerenderer.h"
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <QPainter>
|
||
#include <QPainterPath>
|
||
#include <QToolTip>
|
||
#include <QDebug>
|
||
#include "pathfinder.h"
|
||
#include "yamlparser.h"
|
||
#include "localization.h"
|
||
|
||
GLuint vertexbuffer;
|
||
GLuint VertexArrayID;
|
||
|
||
QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
|
||
{
|
||
setFocusPolicy(Qt::FocusPolicy::StrongFocus);
|
||
setMouseTracking(true);
|
||
installEventFilter(this);
|
||
|
||
QImage img = QImage("/home/bart/git/Ascent-of-Man/images/heightmap.jpg").convertToFormat(QImage::Format_RGB32);
|
||
QImage typeImg = QImage("/home/bart/git/Ascent-of-Man/images/typemap.png").convertToFormat(QImage::Format_RGB32);
|
||
QImage riverImg = QImage("/home/bart/git/Ascent-of-Man/images/rivermap.png").convertToFormat(QImage::Format_RGB32);
|
||
m_zoom = 4;
|
||
m_point = new Matrix2D<TerrainPoint>(img.width(), img.height());
|
||
m_numPoints05 = img.width() / MAP_WIDTH / 1.5 / 2;
|
||
|
||
for (int y = 0; y < img.height(); y++) {
|
||
QRgb *line = (QRgb *) img.scanLine(img.height() - 1 - y);
|
||
QRgb *typeLine = !typeImg.isNull() ? (QRgb *) typeImg.scanLine(typeImg.height() - 1 - y) : nullptr;
|
||
QRgb *riverLine = !riverImg.isNull() ? (QRgb *) riverImg.scanLine(riverImg.height() - 1 - y) : nullptr;
|
||
TerrainPoint *row = m_point->row(y);
|
||
for (int x = 0; x < img.width(); x++) {
|
||
row[x].height = qRed(line[x]);
|
||
row[x].river = (riverLine && qBlue(riverLine[x]) > 120 && qRed(riverLine[x]) < 100) ? 1 : 0;
|
||
if (typeLine) {
|
||
QRgb c = typeLine[x];
|
||
int r = qRed(c), g = qGreen(c), b = qBlue(c);
|
||
int sat = std::max(r, std::max(g, b)) - std::min(r, std::min(g, b));
|
||
if (r > 150 && g < 100) row[x].type = 3; // Prairie / Red Canyon
|
||
else if (r > 150 && g > 150 && b < 100) row[x].type = 1; // Sahara
|
||
else if (b > 50 && r < 50) row[x].type = 4; // Bos (Forest)
|
||
else if (sat < 40 && g < 140) row[x].type = 2; // Tundra
|
||
else row[x].type = 0; // Grasland
|
||
} else {
|
||
row[x].type = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
loadCameraConfig();
|
||
zRot = 90.0f; // Default looking North (+Y)
|
||
m_zoom = 8.0f;
|
||
cameraUpDirection = cameraTransformation * QVector3D(0, 1, 0);
|
||
cameraTransformation.rotate(zRot, 0, 0, 1);
|
||
cameraPosition = QVector3D(28.0f, 16.0f, m_zoom); // South of the alpine mountain, looking North
|
||
|
||
tiles = new Matrix2D<Tile>(MAP_WIDTH, MAP_HEIGHT);
|
||
loadRiverMap(riverImg);
|
||
|
||
m_draggingMinimap = false;
|
||
m_minimapZoom = 1.0f;
|
||
m_image_minimap = QImage("/home/bart/git/Ascent-of-Man/images/minimap.png");
|
||
if (m_image_minimap.isNull()) {
|
||
m_image_minimap = QImage("/home/bart/git/Ascent-of-Man/images/rivermap_preview.png");
|
||
}
|
||
|
||
// Initialize GameManager and spawn demo units
|
||
GameManager::instance().init("/home/bart/git/Ascent-of-Man", m_point, m_numPoints05);
|
||
|
||
m_lastFrameTime = 0.0f;
|
||
m_elapsedTimer.start();
|
||
QTimer *animTimer = new QTimer(this);
|
||
connect(animTimer, &QTimer::timeout, this, QOverload<>::of(&QSceneRenderer::update));
|
||
animTimer->start(16);
|
||
|
||
m_slotLongPressTimer = new QTimer(this);
|
||
m_slotLongPressTimer->setSingleShot(true);
|
||
connect(m_slotLongPressTimer, &QTimer::timeout, this, [this]() {
|
||
onSlotLongPressTimeout();
|
||
});
|
||
}
|
||
|
||
void QSceneRenderer::loadCameraConfig()
|
||
{
|
||
QString configPath = "/home/bart/git/Ascent-of-Man/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;
|
||
|
||
float scaleX = m_numPoints05 * 2.0f;
|
||
float scaleY = m_numPoints05 / LOD_0_SIZE_Y;
|
||
|
||
for (int y = 0; y < MAP_HEIGHT; ++y) {
|
||
for (int x = 0; x < MAP_WIDTH; ++x) {
|
||
float worldX = x * (LOD_0_SIZE * 0.75f);
|
||
float worldY = y * (LOD_0_SIZE_Y * 2.0f) + ((x % 2 == 0) ? 0.0f : LOD_0_SIZE_Y);
|
||
|
||
for (int e = 0; e < 6; ++e) {
|
||
QVector2D mid = getHexEdgeMidpoint(e);
|
||
float midX = worldX + mid.x();
|
||
float midY = worldY + mid.y();
|
||
|
||
int px = (int) round(midX * scaleX);
|
||
int py = (int) round(midY * scaleY);
|
||
|
||
bool hasRiver = false;
|
||
for (int dy = -1; dy <= 1 && !hasRiver; ++dy) {
|
||
for (int dx = -1; dx <= 1 && !hasRiver; ++dx) {
|
||
int sx = px + dx;
|
||
int sy = py + dy;
|
||
if (sx >= 0 && sx < riverImg.width() && sy >= 0 && sy < riverImg.height()) {
|
||
QRgb c = riverImg.pixel(sx, riverImg.height() - 1 - sy);
|
||
if (qBlue(c) > 120 && qRed(c) < 100) {
|
||
hasRiver = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (hasRiver) {
|
||
tiles->operator()(x, y).addRiverEdge(e);
|
||
int nx, ny;
|
||
getHexNeighbor(x, y, e, nx, ny);
|
||
if (nx >= 0 && nx < MAP_WIDTH && ny >= 0 && ny < MAP_HEIGHT) {
|
||
tiles->operator()(nx, ny).addRiverEdge((e + 3) % 6);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Tile * QSceneRenderer::getTile(int x, int y)
|
||
{
|
||
return &tiles->operator()(x, y);
|
||
}
|
||
|
||
QRect QSceneRenderer::getMinimapRect() const
|
||
{
|
||
int maxW = std::min(480, std::max(200, width() / 3));
|
||
int mapW = maxW;
|
||
int mapH = (int) round(mapW / (225.0f / 108.22f));
|
||
if (mapH > 500) mapH = 500;
|
||
if (mapW > 500) mapW = 500;
|
||
int margin = 16;
|
||
return QRect(margin, height() - mapH - margin - 22, mapW, mapH);
|
||
}
|
||
|
||
QRect QSceneRenderer::getMinimapCardRect() const
|
||
{
|
||
QRect m = getMinimapRect();
|
||
return m.adjusted(-6, -6, 6, 24);
|
||
}
|
||
|
||
QRect QSceneRenderer::getZoomOutButtonRect() const
|
||
{
|
||
QRect card = getMinimapCardRect();
|
||
return QRect(card.left() + 4, card.top() - 28, 30, 22);
|
||
}
|
||
|
||
QRect QSceneRenderer::getZoomResetButtonRect() const
|
||
{
|
||
QRect card = getMinimapCardRect();
|
||
return QRect(card.left() + 38, card.top() - 28, 48, 22);
|
||
}
|
||
|
||
QRect QSceneRenderer::getZoomInButtonRect() const
|
||
{
|
||
QRect card = getMinimapCardRect();
|
||
return QRect(card.left() + 90, card.top() - 28, 30, 22);
|
||
}
|
||
|
||
QRect QSceneRenderer::getMinimapSourceRect() const
|
||
{
|
||
if (m_image_minimap.isNull()) return QRect();
|
||
int imgW = m_image_minimap.width();
|
||
int imgH = m_image_minimap.height();
|
||
if (m_minimapZoom <= 1.01f) {
|
||
return QRect(0, 0, imgW, imgH);
|
||
}
|
||
|
||
int srcW = (int) round(imgW / m_minimapZoom);
|
||
int srcH = (int) round(imgH / m_minimapZoom);
|
||
|
||
float u = std::max(0.0f, std::min(1.0f, cameraPosition.x() / 225.0f));
|
||
float v = std::max(0.0f, std::min(1.0f, 1.0f - (cameraPosition.y() / 108.22f)));
|
||
|
||
int camX = (int) round(u * imgW);
|
||
int camY = (int) round(v * imgH);
|
||
|
||
int srcX = camX - srcW / 2;
|
||
int srcY = camY - srcH / 2;
|
||
|
||
if (srcX < 0) srcX = 0;
|
||
if (srcY < 0) srcY = 0;
|
||
if (srcX + srcW > imgW) srcX = imgW - srcW;
|
||
if (srcY + srcH > imgH) srcY = imgH - srcH;
|
||
|
||
return QRect(srcX, srcY, srcW, srcH);
|
||
}
|
||
|
||
QPointF QSceneRenderer::worldToMinimap(float wx, float wy, const QRect &mapRect) const
|
||
{
|
||
QRect src = getMinimapSourceRect();
|
||
if (src.isEmpty() || m_image_minimap.isNull() || m_minimapZoom <= 1.01f) {
|
||
float u = std::max(0.0f, std::min(1.0f, wx / 225.0f));
|
||
float v = std::max(0.0f, std::min(1.0f, 1.0f - (wy / 108.22f)));
|
||
return QPointF(mapRect.left() + u * mapRect.width(), mapRect.top() + v * mapRect.height());
|
||
}
|
||
|
||
float imgW = m_image_minimap.width();
|
||
float imgH = m_image_minimap.height();
|
||
|
||
float uMin = (float)src.left() / imgW;
|
||
float uMax = (float)src.right() / imgW;
|
||
float vMin = (float)src.top() / imgH;
|
||
float vMax = (float)src.bottom() / imgH;
|
||
|
||
float u = wx / 225.0f;
|
||
float v = 1.0f - (wy / 108.22f);
|
||
|
||
float relU = (u - uMin) / (uMax - uMin);
|
||
float relV = (v - vMin) / (vMax - vMin);
|
||
|
||
return QPointF(mapRect.left() + relU * mapRect.width(), mapRect.top() + relV * mapRect.height());
|
||
}
|
||
|
||
QVector2D QSceneRenderer::minimapToWorld(const QPoint &pos, const QRect &mapRect) const
|
||
{
|
||
QRect src = getMinimapSourceRect();
|
||
float relU = std::max(0.0f, std::min(1.0f, (float)(pos.x() - mapRect.left()) / mapRect.width()));
|
||
float relV = std::max(0.0f, std::min(1.0f, (float)(pos.y() - mapRect.top()) / mapRect.height()));
|
||
|
||
if (src.isEmpty() || m_image_minimap.isNull() || m_minimapZoom <= 1.01f) {
|
||
float wx = relU * 225.0f;
|
||
float wy = (1.0f - relV) * 108.22f;
|
||
return QVector2D(wx, wy);
|
||
}
|
||
|
||
float imgW = m_image_minimap.width();
|
||
float imgH = m_image_minimap.height();
|
||
|
||
float uMin = (float)src.left() / imgW;
|
||
float uMax = (float)src.right() / imgW;
|
||
float vMin = (float)src.top() / imgH;
|
||
float vMax = (float)src.bottom() / imgH;
|
||
|
||
float u = uMin + relU * (uMax - uMin);
|
||
float v = vMin + relV * (vMax - vMin);
|
||
|
||
float wx = std::max(0.0f, std::min(225.0f, u * 225.0f));
|
||
float wy = std::max(0.0f, std::min(108.22f, (1.0f - v) * 108.22f));
|
||
return QVector2D(wx, wy);
|
||
}
|
||
|
||
void QSceneRenderer::drawMinimap(QPainter &painter)
|
||
{
|
||
QRect mapRect = getMinimapRect();
|
||
if (mapRect.width() <= 10 || mapRect.height() <= 10) return;
|
||
|
||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||
|
||
// 1. Draw Zoom Buttons above card
|
||
QRect btnOut = getZoomOutButtonRect();
|
||
QRect btnReset = getZoomResetButtonRect();
|
||
QRect btnIn = getZoomInButtonRect();
|
||
|
||
// Zoom Out [-]
|
||
painter.setBrush(QColor(22, 28, 40, 230));
|
||
painter.setPen(QPen(QColor(80, 110, 150, 200), 1.2));
|
||
painter.drawRoundedRect(btnOut, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 10, QFont::Bold));
|
||
painter.setPen(QColor(220, 230, 245));
|
||
painter.drawText(btnOut, Qt::AlignCenter, "-");
|
||
|
||
// Zoom Indicator / Reset [Full / 2.0x]
|
||
painter.setBrush(QColor(22, 28, 40, 230));
|
||
painter.setPen(QPen(QColor(80, 110, 150, 200), 1.2));
|
||
painter.drawRoundedRect(btnReset, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(m_minimapZoom > 1.01f ? QColor(255, 220, 80) : QColor(200, 215, 235));
|
||
QString zoomText = m_minimapZoom <= 1.01f ? "Full" : QString("%1x").arg(m_minimapZoom, 0, 'f', 1);
|
||
painter.drawText(btnReset, Qt::AlignCenter, zoomText);
|
||
|
||
// Zoom In [+]
|
||
painter.setBrush(QColor(22, 28, 40, 230));
|
||
painter.setPen(QPen(QColor(80, 110, 150, 200), 1.2));
|
||
painter.drawRoundedRect(btnIn, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 10, QFont::Bold));
|
||
painter.setPen(QColor(220, 230, 245));
|
||
painter.drawText(btnIn, Qt::AlignCenter, "+");
|
||
|
||
// Title label next to zoom buttons:
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(160, 195, 230));
|
||
QRect titleRect(btnIn.right() + 8, btnIn.top(), 180, btnIn.height());
|
||
painter.drawText(titleRect, Qt::AlignLeft | Qt::AlignVCenter, "HEX MINIMAP");
|
||
|
||
// 2. Semi-transparent dark background card
|
||
QRect cardRect = getMinimapCardRect();
|
||
QColor cardBg(20, 24, 32, 225);
|
||
painter.setBrush(cardBg);
|
||
painter.setPen(QPen(QColor(80, 110, 150, 180), 1.5));
|
||
painter.drawRoundedRect(cardRect, 8, 8);
|
||
|
||
// 3. Draw map image inside mapRect (with source crop for zoom)
|
||
QRect srcRect = getMinimapSourceRect();
|
||
if (!m_image_minimap.isNull()) {
|
||
painter.drawImage(mapRect, m_image_minimap, srcRect);
|
||
} else {
|
||
painter.fillRect(mapRect, QColor(30, 40, 60));
|
||
}
|
||
|
||
// Border around map image
|
||
painter.setBrush(Qt::NoBrush);
|
||
painter.setPen(QPen(QColor(120, 150, 180, 160), 1.0));
|
||
painter.drawRect(mapRect);
|
||
|
||
// Draw roads on minimap
|
||
for (const auto &pair : GameManager::instance().getTileRoads()) {
|
||
const TileRoad &road = pair.second;
|
||
QVector2D center = Pathfinder::hexToWorld(road.tileX, road.tileY);
|
||
QPointF mapP1 = worldToMinimap(center.x(), center.y(), mapRect);
|
||
if (!mapRect.contains(mapP1.toPoint())) continue;
|
||
|
||
const RoadTypeConfig *rCfg = GameManager::instance().roadConfigs().getRoadType(road.roadTypeId);
|
||
QColor roadCol = rCfg ? rCfg->getColor() : QColor(194, 166, 117);
|
||
|
||
painter.setPen(QPen(roadCol, 1.2f));
|
||
if (road.connectionMask == 0) {
|
||
painter.setBrush(roadCol);
|
||
painter.drawEllipse(mapP1, 1.2, 1.2);
|
||
} else {
|
||
for (int e = 0; e < 6; ++e) {
|
||
if (road.hasConnection(e)) {
|
||
QVector2D edgeMid = center + getHexEdgeMidpoint(e);
|
||
QPointF mapP2 = worldToMinimap(edgeMid.x(), edgeMid.y(), mapRect);
|
||
painter.drawLine(mapP1, mapP2);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Draw unit dots on minimap
|
||
for (const auto &u : GameManager::instance().getUnits()) {
|
||
if (u->isDead()) continue;
|
||
QPointF p = worldToMinimap(u->worldPos.x(), u->worldPos.y(), mapRect);
|
||
if (mapRect.contains(p.toPoint())) {
|
||
const Player *pl = GameManager::instance().players().getPlayer(u->playerId);
|
||
QColor dotCol = pl ? pl->primaryColor : Qt::yellow;
|
||
painter.setBrush(dotCol);
|
||
painter.setPen(QPen(Qt::white, 1.0));
|
||
painter.drawEllipse(p, 3.0, 3.0);
|
||
}
|
||
}
|
||
|
||
// 4. Camera view representation (clipped to mapRect):
|
||
QPointF camCenter = worldToMinimap(cameraPosition.x(), cameraPosition.y(), mapRect);
|
||
|
||
float rad = zRot * PI / 180.0f;
|
||
float fovHalf = 25.0f * PI / 180.0f;
|
||
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);
|
||
float rx = cameraPosition.x() + viewDist * cos(rad + fovHalf);
|
||
float ry = cameraPosition.y() + viewDist * sin(rad + fovHalf);
|
||
float cx = cameraPosition.x() + viewDist * cos(rad);
|
||
float cy = cameraPosition.y() + viewDist * sin(rad);
|
||
|
||
QPointF sLeft = worldToMinimap(lx, ly, mapRect);
|
||
QPointF sRight = worldToMinimap(rx, ry, mapRect);
|
||
QPointF sCenter = worldToMinimap(cx, cy, mapRect);
|
||
|
||
painter.save();
|
||
painter.setClipRect(mapRect);
|
||
|
||
QPolygonF cone;
|
||
cone << camCenter << sLeft << sCenter << sRight;
|
||
|
||
painter.setBrush(QColor(255, 230, 50, 60));
|
||
painter.setPen(QPen(QColor(255, 235, 70, 200), 1.5));
|
||
painter.drawPolygon(cone);
|
||
|
||
painter.setPen(QPen(QColor(255, 240, 100, 180), 1.0, Qt::DashLine));
|
||
painter.drawLine(camCenter, sCenter);
|
||
|
||
painter.setBrush(QColor(0, 0, 0, 140));
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawEllipse(camCenter, 5.0, 5.0);
|
||
|
||
painter.setBrush(QColor(255, 50, 50));
|
||
painter.setPen(QPen(Qt::white, 1.5));
|
||
painter.drawEllipse(camCenter, 3.5, 3.5);
|
||
|
||
painter.restore();
|
||
|
||
// 5. North Compass Indicator
|
||
QPoint compassCenter(mapRect.right() - 16, mapRect.top() + 16);
|
||
painter.setBrush(QColor(15, 20, 30, 180));
|
||
painter.setPen(QPen(QColor(100, 140, 180, 200), 1.0));
|
||
painter.drawEllipse(compassCenter, 10, 10);
|
||
painter.setPen(QPen(QColor(240, 50, 50), 2.0));
|
||
painter.drawLine(compassCenter, QPoint(compassCenter.x(), compassCenter.y() - 7));
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(240, 60, 60));
|
||
painter.drawText(QRect(compassCenter.x() - 6, compassCenter.y() - 18, 12, 10), Qt::AlignCenter, "N");
|
||
|
||
// 6. Coordinates bar
|
||
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 | Hoek: %4°")
|
||
.arg(cameraPosition.x(), 0, 'f', 1)
|
||
.arg(cameraPosition.y(), 0, 'f', 1)
|
||
.arg(m_zoom, 0, 'f', 1)
|
||
.arg(pitchDeg, 0, 'f', 0);
|
||
painter.drawText(bottomBar, Qt::AlignLeft | Qt::AlignVCenter, info);
|
||
}
|
||
|
||
QImage QSceneRenderer::getTintedIcon(const QString &path, const QColor &color)
|
||
{
|
||
QString cacheKey = path + "_" + color.name();
|
||
if (m_tintedIconCache.contains(cacheKey)) {
|
||
return m_tintedIconCache[cacheKey];
|
||
}
|
||
|
||
if (!m_iconCache.contains(path)) {
|
||
QString fullPath = path.startsWith("/") ? path : ("/home/bart/git/Ascent-of-Man/" + path);
|
||
QImage loaded(fullPath);
|
||
if (loaded.isNull()) {
|
||
loaded = QImage(32, 32, QImage::Format_ARGB32);
|
||
loaded.fill(Qt::transparent);
|
||
QPainter p(&loaded);
|
||
p.setRenderHint(QPainter::Antialiasing, true);
|
||
p.setBrush(Qt::white);
|
||
p.setPen(Qt::NoPen);
|
||
p.drawEllipse(6, 6, 20, 20);
|
||
}
|
||
m_iconCache[path] = loaded;
|
||
}
|
||
|
||
QImage base = m_iconCache[path];
|
||
QImage tinted(base.size(), QImage::Format_ARGB32);
|
||
tinted.fill(Qt::transparent);
|
||
|
||
QPainter p(&tinted);
|
||
p.drawImage(0, 0, base);
|
||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||
p.fillRect(tinted.rect(), color);
|
||
p.end();
|
||
|
||
m_tintedIconCache[cacheKey] = tinted;
|
||
return tinted;
|
||
}
|
||
|
||
bool QSceneRenderer::screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY)
|
||
{
|
||
float ndcX = (2.0f * screenPos.x()) / width() - 1.0f;
|
||
float ndcY = 1.0f - (2.0f * screenPos.y()) / height();
|
||
|
||
QMatrix4x4 invVP = (Projection * vMatrix).inverted();
|
||
QVector4D pNear = invVP.map(QVector4D(ndcX, ndcY, -1.0f, 1.0f));
|
||
QVector4D pFar = invVP.map(QVector4D(ndcX, ndcY, 1.0f, 1.0f));
|
||
|
||
if (std::abs(pNear.w()) < 1e-6f || std::abs(pFar.w()) < 1e-6f) return false;
|
||
|
||
QVector3D rayStart = pNear.toVector3DAffine();
|
||
QVector3D rayEnd = pFar.toVector3DAffine();
|
||
QVector3D rayDir = (rayEnd - rayStart).normalized();
|
||
|
||
if (rayDir.z() >= -0.001f) return false;
|
||
|
||
float t = (2.0f - rayStart.z()) / rayDir.z();
|
||
if (t < 0.0f) return false;
|
||
|
||
QVector3D hit = rayStart + t * rayDir;
|
||
for (int iter = 0; iter < 4; ++iter) {
|
||
float h = Unit::getTerrainHeight(hit.x(), hit.y(), m_point, m_numPoints05);
|
||
float dt = (h - hit.z()) / rayDir.z();
|
||
hit += dt * rayDir;
|
||
}
|
||
|
||
Pathfinder::worldToHex(hit.x(), hit.y(), outX, outY);
|
||
return (outX >= 0 && outX < MAP_WIDTH && outY >= 0 && outY < MAP_HEIGHT);
|
||
}
|
||
|
||
void QSceneRenderer::drawTickBar(QPainter &painter)
|
||
{
|
||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||
|
||
int barW = 840, barH = 34;
|
||
QRect barRect(16, 16, barW, barH);
|
||
|
||
// Dark glass card
|
||
painter.setBrush(QColor(18, 24, 34, 235));
|
||
painter.setPen(QPen(QColor(70, 100, 140, 180), 1.2));
|
||
painter.drawRoundedRect(barRect, 6, 6);
|
||
|
||
// 1. Movement Tick Info
|
||
float tickDur = GameManager::instance().getTickDuration();
|
||
float curTick = GameManager::instance().getTickTimer();
|
||
float progress = curTick / tickDur;
|
||
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(210, 225, 245));
|
||
QString tickLabel = QString("Tick: %1s / %2s").arg(curTick, 0, 'f', 1).arg(tickDur, 0, 'f', 0);
|
||
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, 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)(130 * progress), 6);
|
||
painter.setBrush(QColor(60, 180, 255));
|
||
painter.drawRoundedRect(progFg, 3, 3);
|
||
|
||
// 2. Player 1 National Resources (Empire-wide; Science shows per-tick rate, not saved stockpile)
|
||
const Player *p1 = GameManager::instance().players().getPlayer(1);
|
||
m_scienceBarHitRect = QRect();
|
||
m_techTreeBtnRect = QRect();
|
||
if (p1) {
|
||
int sciRate = GameManager::instance().calculatePlayerSciencePerTick(1, m_point, m_numPoints05);
|
||
struct ResItem { QString icon; QString text; QColor col; bool isScience; };
|
||
std::vector<ResItem> natRes = {
|
||
{"images/resources/cultuur.png", QString("%L1").arg(p1->culture), QColor(215, 110, 255), false},
|
||
{"images/resources/kennis.png", Localization::instance().get("ui.tech_tree.science_rate", "+%L1/tick").arg(sciRate), QColor(60, 190, 255), true},
|
||
{"images/resources/goud.png", QString("%L1").arg(p1->gold), QColor(255, 210, 50), false},
|
||
{"images/resources/geloof.png", QString("%L1").arg(p1->faith), QColor(255, 230, 80), false}
|
||
};
|
||
|
||
int startX = barRect.left() + 150;
|
||
int itemW = 92;
|
||
for (size_t i = 0; i < natRes.size(); ++i) {
|
||
int ix = startX + (int)i * itemW;
|
||
QRect slotRect(ix - 2, barRect.top() + 4, itemW - 2, 26);
|
||
if (natRes[i].isScience) {
|
||
m_scienceBarHitRect = slotRect;
|
||
}
|
||
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, natRes[i].text);
|
||
}
|
||
|
||
// 2b. Active Research / Tech Tree Button
|
||
m_techTreeBtnRect = QRect(barRect.left() + 520, barRect.top() + 5, 158, 24);
|
||
bool treeOpen = GameManager::instance().isTechTreeOpen();
|
||
bool hasRes = p1->hasActiveResearch();
|
||
painter.setBrush(treeOpen ? QColor(35, 80, 130, 245) : (hasRes ? QColor(22, 42, 68, 235) : QColor(45, 35, 25, 235)));
|
||
painter.setPen(QPen(treeOpen ? QColor(100, 220, 255) : (hasRes ? QColor(70, 175, 240) : QColor(255, 190, 60)), 1.3));
|
||
painter.drawRoundedRect(m_techTreeBtnRect, 4, 4);
|
||
|
||
if (hasRes) {
|
||
QString activeTechId = p1->getActiveResearchTech();
|
||
const TechConfig *tCfg = GameManager::instance().techTree().getTech(activeTechId);
|
||
QString tName = tCfg ? tCfg->name : activeTechId;
|
||
int prog = p1->getTechProgress(activeTechId);
|
||
int cost = tCfg ? std::max(1, tCfg->scienceCost) : 1;
|
||
float frac = std::max(0.0f, std::min(1.0f, (float)prog / (float)cost));
|
||
int pct = (int)(frac * 100.0f);
|
||
|
||
// Mini progress bar at bottom of button
|
||
QRect miniBg(m_techTreeBtnRect.left() + 4, m_techTreeBtnRect.bottom() - 4, m_techTreeBtnRect.width() - 8, 3);
|
||
painter.setBrush(QColor(15, 22, 32));
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawRoundedRect(miniBg, 1, 1);
|
||
|
||
QRect miniFg(m_techTreeBtnRect.left() + 4, m_techTreeBtnRect.bottom() - 4, (int)((m_techTreeBtnRect.width() - 8) * frac), 3);
|
||
painter.setBrush(QColor(60, 210, 255));
|
||
painter.drawRoundedRect(miniFg, 1, 1);
|
||
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(220, 245, 255));
|
||
QString btnText = QString("%1 (%2%) [T]").arg(tName).arg(pct);
|
||
painter.drawText(m_techTreeBtnRect.adjusted(4, 0, -4, -3), Qt::AlignCenter, btnText);
|
||
} else {
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(255, 215, 90));
|
||
painter.drawText(m_techTreeBtnRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.tech_tree.choose_research", "Choose Tech [T]"));
|
||
}
|
||
}
|
||
|
||
// 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);
|
||
m_speed5xRect = QRect(barRect.right() - 54, barRect.top() + 6, 42, 22);
|
||
|
||
auto drawSpeedBtn = [&](const QRect &r, const QString &text, bool active) {
|
||
painter.setBrush(active ? QColor(40, 70, 110, 240) : QColor(25, 32, 45, 220));
|
||
painter.setPen(QPen(active ? QColor(255, 215, 60) : QColor(80, 110, 150), active ? 1.5 : 1.0));
|
||
painter.drawRoundedRect(r, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(active ? QColor(255, 225, 80) : QColor(200, 215, 235));
|
||
painter.drawText(r, Qt::AlignCenter, text);
|
||
};
|
||
|
||
drawSpeedBtn(m_speed1xRect, "1x", std::abs(curSpeed - 1.0f) < 0.1f);
|
||
drawSpeedBtn(m_speed2xRect, "2x", std::abs(curSpeed - 2.0f) < 0.1f);
|
||
drawSpeedBtn(m_speed5xRect, "5x", std::abs(curSpeed - 5.0f) < 0.1f);
|
||
}
|
||
|
||
void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
|
||
{
|
||
m_activeActionBubbles.clear();
|
||
m_cityBannerHits.clear();
|
||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||
|
||
Unit *selectedUnit = GameManager::instance().getSelectedUnit();
|
||
|
||
// 1. Draw City Banners (if not in city view)
|
||
if (!GameManager::instance().isInCityView()) {
|
||
for (const auto &c : GameManager::instance().getCities()) {
|
||
bool inFront = false;
|
||
QPointF screenP = UnitRenderer::projectToScreen(c.worldPos + QVector3D(0, 0, 0.65f), Projection, vMatrix, width(), height(), inFront);
|
||
if (!inFront) continue;
|
||
|
||
const Player *pl = GameManager::instance().players().getPlayer(c.playerId);
|
||
QColor col = pl ? pl->primaryColor : Qt::blue;
|
||
|
||
int bWidth = c.hasActiveProject() ? 150 : 110;
|
||
if (c.hasActiveProject() && !c.projectQueue.empty()) {
|
||
bWidth += 35;
|
||
}
|
||
QRect bannerRect((int)screenP.x() - bWidth / 2, (int)screenP.y() - 14, bWidth, 22);
|
||
painter.setBrush(QColor(20, 24, 34, 235));
|
||
painter.setPen(QPen(col, 2.0));
|
||
painter.drawRoundedRect(bannerRect, 4, 4);
|
||
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(Qt::white);
|
||
QString bannerText = QString("%1 (%2 Pop)").arg(c.name).arg(c.getTotalPop());
|
||
if (c.hasActiveProject()) {
|
||
int pct = (int)(c.currentProject.getProgress() * 100.0f);
|
||
bannerText += QString(" | %1 %2%").arg(c.currentProject.name).arg(pct);
|
||
if (!c.projectQueue.empty()) {
|
||
bannerText += QString(" (+%1)").arg(c.projectQueue.size());
|
||
}
|
||
}
|
||
painter.drawText(bannerRect, Qt::AlignCenter, bannerText);
|
||
|
||
m_cityBannerHits.push_back({bannerRect, c.id});
|
||
}
|
||
}
|
||
|
||
// 2. Draw Units: Teardrop (Druppel in secondary color) + Icon (in primary color)
|
||
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);
|
||
if (!inFront) continue;
|
||
|
||
if (tipPos.x() < -60 || tipPos.x() > width() + 60 || tipPos.y() < -60 || tipPos.y() > height() + 60) {
|
||
continue;
|
||
}
|
||
|
||
const Player *player = GameManager::instance().players().getPlayer(unit->playerId);
|
||
QColor primaryCol = player ? player->primaryColor : Qt::blue;
|
||
QColor secondaryCol = player ? player->secondaryColor : Qt::white;
|
||
|
||
QPointF headCenter(tipPos.x(), tipPos.y() - 25.0f);
|
||
|
||
// Teardrop path (pointing down to top of cylinder)
|
||
QPainterPath teardrop;
|
||
teardrop.moveTo(tipPos);
|
||
teardrop.lineTo(headCenter.x() - R * 0.88f, headCenter.y() + R * 0.45f);
|
||
teardrop.arcTo(headCenter.x() - R, headCenter.y() - R, R * 2.0f, R * 2.0f, 205.0f, -230.0f);
|
||
teardrop.lineTo(tipPos);
|
||
teardrop.closeSubpath();
|
||
|
||
// Drop shadow
|
||
painter.save();
|
||
painter.translate(1.0, 1.5);
|
||
painter.fillPath(teardrop, QColor(0, 0, 0, 70));
|
||
painter.restore();
|
||
|
||
// Fill teardrop with secondary color
|
||
painter.fillPath(teardrop, secondaryCol);
|
||
|
||
// Border of teardrop
|
||
painter.setPen(QPen(QColor(25, 30, 40, 220), 1.6));
|
||
painter.drawPath(teardrop);
|
||
|
||
// Inside the druppel: Draw unit icon tinted in player primary color
|
||
QImage icon = getTintedIcon(unit->config.iconPath, primaryCol);
|
||
QRectF iconRect(headCenter.x() - 9.0f, headCenter.y() - 9.0f, 18.0f, 18.0f);
|
||
painter.drawImage(iconRect, icon);
|
||
|
||
// Health bar above head
|
||
float hpFrac = std::max(0.0f, std::min(1.0f, unit->health / unit->maxHealth));
|
||
QRectF hpBg(headCenter.x() - 14.0f, headCenter.y() - R - 6.0f, 28.0f, 3.5f);
|
||
painter.setBrush(QColor(30, 30, 30, 200));
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawRoundedRect(hpBg, 1.5, 1.5);
|
||
|
||
QRectF hpFg(headCenter.x() - 14.0f, headCenter.y() - R - 6.0f, 28.0f * hpFrac, 3.5f);
|
||
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;
|
||
painter.setPen(QPen(QColor(255, 220, 40, (int)(180 + pulse * 75)), 2.5));
|
||
painter.setBrush(Qt::NoBrush);
|
||
painter.drawEllipse(headCenter, R + 3.0f, R + 3.0f);
|
||
|
||
// Floating Action Bubbles above the unit's druppel
|
||
const QStringList &actions = unit->config.actions;
|
||
int numActions = actions.size();
|
||
float bubbleR = 12.0f;
|
||
float spacing = 28.0f;
|
||
float startX = headCenter.x() - ((numActions - 1) * spacing) / 2.0f;
|
||
float bubbleY = headCenter.y() - R - 20.0f;
|
||
|
||
for (int a = 0; a < numActions; ++a) {
|
||
QString act = actions[a];
|
||
QPointF bCenter(startX + a * spacing, bubbleY);
|
||
QRect bRect((int)(bCenter.x() - bubbleR), (int)(bCenter.y() - bubbleR), (int)(bubbleR * 2), (int)(bubbleR * 2));
|
||
|
||
bool isActive = false;
|
||
QString iconFile = "images/actions/move.png";
|
||
QString tip = "Verplaatsen (M)";
|
||
if (act == "verplaatsen") {
|
||
isActive = (GameManager::instance().currentActionMode() == ActionMode::Move);
|
||
iconFile = "images/actions/move.png";
|
||
tip = "Verplaatsen (M)";
|
||
} else if (act == "aanvallen") {
|
||
isActive = (GameManager::instance().currentActionMode() == ActionMode::Attack);
|
||
iconFile = "images/actions/attack.png";
|
||
tip = Localization::instance().get("ui.unit.tip_attack", "Attack (A) [Range: %1]").arg(unit->config.attackRange);
|
||
} else if (act == "steden_bouwen" || act == "build_city") {
|
||
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildCity);
|
||
isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity) || isQueued;
|
||
iconFile = "images/actions/build_city.png";
|
||
tip = isQueued
|
||
? Localization::instance().get("ui.unit.city_queued", "Found City queued on arrival")
|
||
: Localization::instance().get("ui.unit.tip_build_city", "Found City (B)");
|
||
} else if (act == "boerderij_bouwen" || act == "build_farm") {
|
||
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement);
|
||
isActive = (unit->isBuilding() && unit->buildingImprovementId != "road") || isQueued;
|
||
iconFile = "images/actions/farm.png";
|
||
if (unit->isBuilding() && unit->buildingImprovementId != "road") {
|
||
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
|
||
tip = Localization::instance().get("ui.unit.tip_farm_building", "Busy building... %1%").arg(pct);
|
||
} else if (isQueued) {
|
||
tip = Localization::instance().get("ui.unit.tip_farm_queued", "Build Farm (Queued on arrival - click to cancel)");
|
||
} else {
|
||
tip = Localization::instance().get("ui.unit.tip_build_farm", "Build Farm (F)");
|
||
}
|
||
} else if (act == "build_road" || act == "weg_bouwen") {
|
||
bool isMode = (GameManager::instance().currentActionMode() == ActionMode::BuildRoad);
|
||
bool isBuildingRoad = unit->isBuilding() && unit->buildingImprovementId.startsWith("road");
|
||
bool hasRoute = unit->hasRoadRoute();
|
||
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildRoad);
|
||
isActive = isMode || isBuildingRoad || hasRoute || isQueued;
|
||
iconFile = "images/actions/road.png";
|
||
if (isBuildingRoad) {
|
||
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
|
||
tip = Localization::instance().get("ui.unit.tip_road_building", "Busy building road... %1%").arg(pct);
|
||
} else if (hasRoute) {
|
||
int cur = (int)unit->roadRouteIndex + 1;
|
||
int total = (int)unit->roadRoute.size();
|
||
tip = Localization::instance().get("ui.unit.status_road_route", "Road Route: %1/%2").arg(cur).arg(total);
|
||
} else if (isQueued) {
|
||
tip = Localization::instance().get("ui.unit.tip_road_queued", "Build Road (Queued on arrival - click to cancel)");
|
||
} else {
|
||
tip = Localization::instance().get("ui.unit.tip_build_road", "Build Road (R) - Click target tile to build route");
|
||
}
|
||
}
|
||
|
||
// Draw action bubble
|
||
painter.setBrush(isActive ? QColor(40, 80, 140, 240) : QColor(20, 26, 38, 230));
|
||
painter.setPen(QPen(isActive ? QColor(255, 220, 50) : secondaryCol, isActive ? 2.0 : 1.2));
|
||
painter.drawEllipse(bCenter, bubbleR, bubbleR);
|
||
|
||
// Action icon inside bubble
|
||
QImage aIcon = getTintedIcon(iconFile, isActive ? QColor(255, 230, 80) : primaryCol);
|
||
painter.drawImage(QRectF(bCenter.x() - 7, bCenter.y() - 7, 14, 14), aIcon);
|
||
|
||
// Register for click detection
|
||
m_activeActionBubbles.push_back({bRect, act, tip});
|
||
|
||
// Tooltip on hover
|
||
if (bRect.contains(m_mouseCurrentPos)) {
|
||
QRect tipRect((int)bCenter.x() - 55, (int)bCenter.y() - 26, 110, 18);
|
||
painter.setBrush(QColor(15, 20, 30, 240));
|
||
painter.setPen(QPen(QColor(120, 160, 210), 1.0));
|
||
painter.drawRoundedRect(tipRect, 3, 3);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(Qt::white);
|
||
painter.drawText(tipRect, Qt::AlignCenter, tip);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::drawPathAndRange(QPainter &painter, Unit *unit, const QMatrix4x4 &vMatrix)
|
||
{
|
||
if (!unit) return;
|
||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||
|
||
// 1. If unit is moving or has a path: draw path line and destination reticle
|
||
if (unit->path.size() > 1) {
|
||
QPainterPath pathLine;
|
||
bool first = true;
|
||
for (const auto &p : unit->path) {
|
||
QVector2D w = Pathfinder::hexToWorld(p.first, p.second);
|
||
float z = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05);
|
||
bool inFront = false;
|
||
QPointF sPos = UnitRenderer::projectToScreen(QVector3D(w.x(), w.y(), z + 0.05f), Projection, vMatrix, width(), height(), inFront);
|
||
if (inFront) {
|
||
if (first) {
|
||
pathLine.moveTo(sPos);
|
||
first = false;
|
||
} else {
|
||
pathLine.lineTo(sPos);
|
||
}
|
||
}
|
||
}
|
||
painter.setPen(QPen(QColor(80, 220, 255, 200), 2.2, Qt::DashLine));
|
||
painter.setBrush(Qt::NoBrush);
|
||
painter.drawPath(pathLine);
|
||
|
||
// Destination target reticle
|
||
const auto &lastHex = unit->path.back();
|
||
QVector2D destW = Pathfinder::hexToWorld(lastHex.first, lastHex.second);
|
||
float destZ = Unit::getTerrainHeight(destW.x(), destW.y(), m_point, m_numPoints05);
|
||
bool inFront = false;
|
||
QPointF sDest = UnitRenderer::projectToScreen(QVector3D(destW.x(), destW.y(), destZ + 0.05f), Projection, vMatrix, width(), height(), inFront);
|
||
if (inFront) {
|
||
painter.setPen(QPen(QColor(255, 220, 60), 2.0));
|
||
painter.drawEllipse(sDest, 10.0, 10.0);
|
||
painter.drawLine(sDest.x() - 14, sDest.y(), sDest.x() + 14, sDest.y());
|
||
painter.drawLine(sDest.x(), sDest.y() - 14, sDest.x(), sDest.y() + 14);
|
||
}
|
||
}
|
||
|
||
// 2. Road Route visualization if unit is constructing a multi-tile road route
|
||
if (unit->hasRoadRoute()) {
|
||
QPainterPath routeLine;
|
||
bool first = true;
|
||
for (size_t i = unit->roadRouteIndex; i < unit->roadRoute.size(); ++i) {
|
||
auto hex = unit->roadRoute[i];
|
||
QVector2D w = Pathfinder::hexToWorld(hex.first, hex.second);
|
||
float z = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05);
|
||
bool inFront = false;
|
||
QPointF sPos = UnitRenderer::projectToScreen(QVector3D(w.x(), w.y(), z + 0.06f), Projection, vMatrix, width(), height(), inFront);
|
||
if (inFront) {
|
||
if (first) {
|
||
routeLine.moveTo(sPos);
|
||
first = false;
|
||
} else {
|
||
routeLine.lineTo(sPos);
|
||
}
|
||
painter.setPen(QPen(QColor(140, 95, 30, 200), 1.2));
|
||
painter.setBrush(QColor(255, 215, 60, 220));
|
||
painter.drawEllipse(sPos, 3.5, 3.5);
|
||
}
|
||
}
|
||
painter.setPen(QPen(QColor(255, 195, 45, 230), 2.8, Qt::DashLine));
|
||
painter.setBrush(Qt::NoBrush);
|
||
painter.drawPath(routeLine);
|
||
|
||
auto destHex = unit->roadRoute.back();
|
||
QVector2D destW = Pathfinder::hexToWorld(destHex.first, destHex.second);
|
||
float destZ = Unit::getTerrainHeight(destW.x(), destW.y(), m_point, m_numPoints05);
|
||
bool inFront = false;
|
||
QPointF sDest = UnitRenderer::projectToScreen(QVector3D(destW.x(), destW.y(), destZ + 0.06f), Projection, vMatrix, width(), height(), inFront);
|
||
if (inFront) {
|
||
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.008f) + 1.0f) * 0.5f;
|
||
painter.setPen(QPen(QColor(255, 215, 50, (int)(180 + pulse * 75)), 2.2));
|
||
painter.setBrush(QColor(255, 190, 40, 45));
|
||
painter.drawEllipse(sDest, 14.0f, 14.0f);
|
||
painter.drawLine(sDest.x() - 18, sDest.y(), sDest.x() + 18, sDest.y());
|
||
painter.drawLine(sDest.x(), sDest.y() - 18, sDest.x(), sDest.y() + 18);
|
||
}
|
||
}
|
||
|
||
// 3. Road Build targeting preview when in ActionMode::BuildRoad
|
||
if (GameManager::instance().currentActionMode() == ActionMode::BuildRoad && unit->config.canBuildRoad()) {
|
||
int hx = -1, hy = -1;
|
||
if (screenToHex(m_mouseCurrentPos, vMatrix, hx, hy) && (hx != unit->tileX || hy != unit->tileY)) {
|
||
if (TerrainMovementManager::instance().isHexPassable(hx, hy, MovementDomain::Land, m_point, m_numPoints05, &unit->config)) {
|
||
auto preview = Pathfinder::findPath(unit->tileX, unit->tileY, hx, hy, m_point, m_numPoints05, MovementDomain::Land, &unit->config);
|
||
if (preview.size() > 1) {
|
||
QPainterPath previewLine;
|
||
bool first = true;
|
||
for (const auto &p : preview) {
|
||
QVector2D w = Pathfinder::hexToWorld(p.first, p.second);
|
||
float z = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05);
|
||
bool inFront = false;
|
||
QPointF sPos = UnitRenderer::projectToScreen(QVector3D(w.x(), w.y(), z + 0.06f), Projection, vMatrix, width(), height(), inFront);
|
||
if (inFront) {
|
||
if (first) {
|
||
previewLine.moveTo(sPos);
|
||
first = false;
|
||
} else {
|
||
previewLine.lineTo(sPos);
|
||
}
|
||
painter.setPen(QPen(QColor(160, 110, 40, 200), 1.0));
|
||
painter.setBrush(QColor(255, 230, 90, 230));
|
||
painter.drawEllipse(sPos, 3.5, 3.5);
|
||
}
|
||
}
|
||
painter.setPen(QPen(QColor(255, 220, 60, 240), 3.0, Qt::DashLine));
|
||
painter.setBrush(Qt::NoBrush);
|
||
painter.drawPath(previewLine);
|
||
|
||
// Destination reticle
|
||
QVector2D destW = Pathfinder::hexToWorld(hx, hy);
|
||
float destZ = Unit::getTerrainHeight(destW.x(), destW.y(), m_point, m_numPoints05);
|
||
bool inFront = false;
|
||
QPointF sDest = UnitRenderer::projectToScreen(QVector3D(destW.x(), destW.y(), destZ + 0.06f), Projection, vMatrix, width(), height(), inFront);
|
||
if (inFront) {
|
||
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.01f) + 1.0f) * 0.5f;
|
||
painter.setPen(QPen(QColor(255, 225, 70, (int)(180 + pulse * 75)), 2.5));
|
||
painter.setBrush(QColor(255, 210, 50, 50));
|
||
painter.drawEllipse(sDest, 16.0f, 16.0f);
|
||
painter.drawLine(sDest.x() - 22, sDest.y(), sDest.x() + 22, sDest.y());
|
||
painter.drawLine(sDest.x(), sDest.y() - 22, sDest.x(), sDest.y() + 22);
|
||
|
||
QString targetTip = Localization::instance().get("ui.unit.btn_road_route", "Route (%1 hexes)").arg(preview.size() - 1);
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
QFontMetrics fm = painter.fontMetrics();
|
||
int tw = fm.horizontalAdvance(targetTip) + 16;
|
||
QRect tipRect((int)sDest.x() - tw / 2, (int)sDest.y() - 34, tw, 20);
|
||
painter.setBrush(QColor(20, 24, 34, 230));
|
||
painter.setPen(QPen(QColor(255, 215, 60), 1.2));
|
||
painter.drawRoundedRect(tipRect, 4, 4);
|
||
painter.setPen(QColor(255, 235, 120));
|
||
painter.drawText(tipRect, Qt::AlignCenter, targetTip);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4. Attack range visualization if Attack mode is active
|
||
if (GameManager::instance().currentActionMode() == ActionMode::Attack) {
|
||
int range = unit->config.attackRange;
|
||
for (const auto &u : GameManager::instance().getUnits()) {
|
||
if (u->isDead() || u->playerId == unit->playerId) continue;
|
||
|
||
int d = Pathfinder::hexDistance(unit->tileX, unit->tileY, u->tileX, u->tileY);
|
||
if (d <= range) {
|
||
// Target reticle over enemy unit
|
||
bool inFront = false;
|
||
QPointF sTarget = UnitRenderer::projectToScreen(u->worldPos + QVector3D(0, 0, 0.4f), Projection, vMatrix, width(), height(), inFront);
|
||
if (inFront) {
|
||
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.01f) + 1.0f) * 0.5f;
|
||
painter.setPen(QPen(QColor(255, 50, 50, (int)(180 + pulse * 75)), 2.2));
|
||
painter.setBrush(QColor(255, 40, 40, 40));
|
||
painter.drawEllipse(sTarget, 16.0f, 16.0f);
|
||
painter.drawLine(sTarget.x() - 20, sTarget.y(), sTarget.x() + 20, sTarget.y());
|
||
painter.drawLine(sTarget.x(), sTarget.y() - 20, sTarget.x(), sTarget.y() + 20);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix)
|
||
{
|
||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||
for (const auto &ft : GameManager::instance().getFloatingTexts()) {
|
||
bool inFront = false;
|
||
QPointF sPos = UnitRenderer::projectToScreen(ft.worldPos, Projection, vMatrix, width(), height(), inFront);
|
||
if (!inFront) continue;
|
||
|
||
float alpha = std::min(1.0f, ft.life / (ft.maxLife * 0.5f));
|
||
QColor col = ft.color;
|
||
col.setAlphaF(alpha);
|
||
|
||
painter.setFont(QFont("sans-serif", 10, QFont::Bold));
|
||
painter.setPen(QPen(QColor(0, 0, 0, (int)(alpha * 200)), 3.0));
|
||
// Stroke
|
||
QRect textRect((int)sPos.x() - 80, (int)sPos.y() - 12, 160, 24);
|
||
painter.drawText(textRect, Qt::AlignCenter, ft.text);
|
||
|
||
// Fill
|
||
painter.setPen(col);
|
||
painter.drawText(textRect, Qt::AlignCenter, ft.text);
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
|
||
{
|
||
if (!unit) return;
|
||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||
|
||
int cardW = 310, cardH = 180;
|
||
QRect cardRect(width() - cardW - 16, height() - cardH - 16, cardW, cardH);
|
||
m_unitCardRect = cardRect;
|
||
|
||
// Dark glass card
|
||
painter.setBrush(QColor(18, 24, 34, 235));
|
||
painter.setPen(QPen(QColor(80, 115, 160, 200), 1.5));
|
||
painter.drawRoundedRect(cardRect, 8, 8);
|
||
|
||
const Player *player = GameManager::instance().players().getPlayer(unit->playerId);
|
||
QColor primaryCol = player ? player->primaryColor : Qt::blue;
|
||
QColor secondaryCol = player ? player->secondaryColor : Qt::white;
|
||
|
||
// Header badge
|
||
QRect badgeRect(cardRect.left() + 12, cardRect.top() + 12, 22, 22);
|
||
painter.setBrush(secondaryCol);
|
||
painter.setPen(QPen(QColor(30, 30, 30), 1.2));
|
||
painter.drawEllipse(badgeRect);
|
||
QImage uIcon = getTintedIcon(unit->config.iconPath, primaryCol);
|
||
painter.drawImage(QRect(badgeRect.left() + 3, badgeRect.top() + 3, 16, 16), uIcon);
|
||
|
||
// Unit Name & Player
|
||
painter.setFont(QFont("sans-serif", 11, QFont::Bold));
|
||
painter.setPen(QColor(240, 245, 255));
|
||
painter.drawText(QRect(badgeRect.right() + 10, cardRect.top() + 10, 160, 18), Qt::AlignLeft | Qt::AlignVCenter, unit->config.name.toUpper());
|
||
|
||
painter.setFont(QFont("sans-serif", 8));
|
||
painter.setPen(QColor(160, 185, 215));
|
||
QString playerInfo = QString("%1 | Hex: (%2, %3)").arg(player ? player->name : "Onbekend").arg(unit->tileX).arg(unit->tileY);
|
||
painter.drawText(QRect(badgeRect.right() + 10, cardRect.top() + 28, 200, 14), Qt::AlignLeft | Qt::AlignVCenter, playerInfo);
|
||
|
||
// Category Pill
|
||
QString catStr = unitCategoryToString(unit->config.category);
|
||
if (unit->config.isRanged()) catStr += " (Ranged)";
|
||
QRect catRect(cardRect.right() - 110, cardRect.top() + 12, 98, 18);
|
||
painter.setBrush(QColor(35, 48, 68, 200));
|
||
painter.setPen(QPen(QColor(100, 140, 190), 1.0));
|
||
painter.drawRoundedRect(catRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(200, 225, 255));
|
||
painter.drawText(catRect, Qt::AlignCenter, catStr);
|
||
|
||
// Separator line
|
||
painter.setPen(QPen(QColor(60, 85, 120, 150), 1.0));
|
||
painter.drawLine(cardRect.left() + 12, cardRect.top() + 48, cardRect.right() - 12, cardRect.top() + 48);
|
||
|
||
// Health Bar
|
||
float hpFrac = std::max(0.0f, std::min(1.0f, unit->health / unit->maxHealth));
|
||
QRect hpBg(cardRect.left() + 12, cardRect.top() + 56, 170, 14);
|
||
painter.setBrush(QColor(30, 40, 55));
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawRoundedRect(hpBg, 3, 3);
|
||
|
||
QRect hpFg(cardRect.left() + 12, cardRect.top() + 56, (int)(170 * hpFrac), 14);
|
||
painter.setBrush(hpFrac > 0.5f ? QColor(60, 200, 80) : (hpFrac > 0.25f ? QColor(255, 180, 40) : QColor(240, 50, 50)));
|
||
painter.drawRoundedRect(hpFg, 3, 3);
|
||
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(Qt::white);
|
||
QString hpText = QString("HP: %1 / %2").arg((int)unit->health).arg((int)unit->maxHealth);
|
||
painter.drawText(hpBg, Qt::AlignCenter, hpText);
|
||
|
||
// Stats Grid
|
||
painter.setFont(QFont("sans-serif", 8));
|
||
painter.setPen(QColor(200, 220, 240));
|
||
|
||
QString speedInfo = Localization::instance().get("ui.unit.speed_format", "Speed: %1 hex/tick").arg(unit->config.movementSpeed, 0, 'f', 1);
|
||
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 76, 140, 14), Qt::AlignLeft | Qt::AlignVCenter, speedInfo);
|
||
|
||
QString costInfo = Localization::instance().get("ui.unit.cost_format", "Cost: %1 pts").arg(unit->config.cost);
|
||
painter.drawText(QRect(cardRect.left() + 160, cardRect.top() + 76, 130, 14), Qt::AlignLeft | Qt::AlignVCenter, costInfo);
|
||
|
||
if (unit->config.isMilitary()) {
|
||
QString combatInfo = Localization::instance().get("ui.unit.combat_format", "Attack: %1 | Range: %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 = Localization::instance().get("ui.unit.building_pct", "Building... %1%").arg(pct);
|
||
if (unit->hasRoadRoute()) {
|
||
int cur = (int)unit->roadRouteIndex + 1;
|
||
int total = (int)unit->roadRoute.size();
|
||
buildInfo += QString(" | ") + Localization::instance().get("ui.unit.status_road_route", "Road Route: %1/%2").arg(cur).arg(total);
|
||
} else if (!unit->config.infiniteCharges && unit->buildCharges > 0) {
|
||
buildInfo += QString(" | ") + Localization::instance().get("ui.unit.charges_format", "Charges: %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->hasRoadRoute()) {
|
||
int cur = (int)unit->roadRouteIndex + 1;
|
||
int total = (int)unit->roadRoute.size();
|
||
QString routeInfo = Localization::instance().get("ui.unit.status_road_route", "Road Route: %1/%2").arg(cur).arg(total);
|
||
painter.setPen(QColor(255, 200, 70));
|
||
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, routeInfo);
|
||
} else if (unit->hasQueuedAction()) {
|
||
QString qSummary = unit->getQueuedActionSummary();
|
||
QString queuedInfo = Localization::instance().get("ui.unit.status_queued", "Queued: %1").arg(qSummary);
|
||
if (unit->config.isBuilder() && !unit->config.infiniteCharges && unit->buildCharges > 0) {
|
||
queuedInfo += QString(" | ") + Localization::instance().get("ui.unit.charges_format", "Charges: %1").arg(unit->buildCharges);
|
||
}
|
||
painter.setPen(QColor(100, 220, 140));
|
||
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, queuedInfo);
|
||
} else if (unit->config.isBuilder() && !unit->config.infiniteCharges && unit->buildCharges > 0) {
|
||
QString builderInfo = Localization::instance().get("ui.unit.civilian_unit", "Civilian unit") + QString(" | ") +
|
||
Localization::instance().get("ui.unit.charges_format", "Charges: %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,
|
||
Localization::instance().get("ui.unit.civilian_unit", "Civilian unit"));
|
||
}
|
||
|
||
// Action Bubbles Row at bottom of card
|
||
QRect actionArea(cardRect.left() + 12, cardRect.top() + 116, cardRect.width() - 24, 52);
|
||
painter.setBrush(QColor(12, 16, 24, 180));
|
||
painter.setPen(QPen(QColor(50, 75, 105, 120), 1.0));
|
||
painter.drawRoundedRect(actionArea, 6, 6);
|
||
|
||
const QStringList &actions = unit->config.actions;
|
||
int numActions = actions.size();
|
||
int btnW = (actionArea.width() - 8 * (numActions + 1)) / std::max(1, numActions);
|
||
|
||
for (int a = 0; a < numActions; ++a) {
|
||
QString act = actions[a];
|
||
QRect bRect(actionArea.left() + 8 + a * (btnW + 8), actionArea.top() + 6, btnW, 40);
|
||
|
||
bool isActive = false;
|
||
QString iconFile = "images/actions/move.png";
|
||
QString label = Localization::instance().get("ui.unit.btn_move", "Move [M]");
|
||
QString tip = Localization::instance().get("ui.unit.tip_move", "Move (M)");
|
||
|
||
if (act == "verplaatsen") {
|
||
isActive = (GameManager::instance().currentActionMode() == ActionMode::Move);
|
||
iconFile = "images/actions/move.png";
|
||
label = Localization::instance().get("ui.unit.btn_move", "Move [M]");
|
||
tip = Localization::instance().get("ui.unit.tip_move", "Move (M)");
|
||
} else if (act == "aanvallen") {
|
||
isActive = (GameManager::instance().currentActionMode() == ActionMode::Attack);
|
||
iconFile = "images/actions/attack.png";
|
||
label = Localization::instance().get("ui.unit.btn_attack", "Attack [A]");
|
||
tip = Localization::instance().get("ui.unit.tip_attack", "Attack (A) [Range: %1]").arg(unit->config.attackRange);
|
||
} else if (act == "steden_bouwen" || act == "build_city") {
|
||
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildCity);
|
||
isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity) || isQueued;
|
||
iconFile = "images/actions/build_city.png";
|
||
label = isQueued ? "Queued" : Localization::instance().get("ui.unit.btn_build_city", "Settle [B]");
|
||
tip = isQueued
|
||
? Localization::instance().get("ui.unit.city_queued", "Found City queued on arrival")
|
||
: Localization::instance().get("ui.unit.tip_build_city", "Found City (B)");
|
||
} else if (act == "boerderij_bouwen" || act == "build_farm") {
|
||
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement);
|
||
isActive = (unit->isBuilding() && unit->buildingImprovementId != "road") || isQueued;
|
||
iconFile = "images/actions/farm.png";
|
||
if (unit->isBuilding() && unit->buildingImprovementId != "road") {
|
||
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
|
||
label = Localization::instance().get("ui.unit.btn_farm_building", "Building %1%").arg(pct);
|
||
tip = Localization::instance().get("ui.unit.tip_farm_building", "Busy building... %1%").arg(pct);
|
||
} else if (isQueued) {
|
||
label = Localization::instance().get("ui.unit.btn_farm_queued", "Farm (Queued)");
|
||
tip = Localization::instance().get("ui.unit.tip_farm_queued", "Build Farm (Queued on arrival - click to cancel)");
|
||
} else {
|
||
label = Localization::instance().get("ui.unit.btn_build_farm", "Farm [F]");
|
||
tip = Localization::instance().get("ui.unit.tip_build_farm", "Build Farm (F)");
|
||
}
|
||
} else if (act == "build_road" || act == "weg_bouwen") {
|
||
bool isMode = (GameManager::instance().currentActionMode() == ActionMode::BuildRoad);
|
||
bool isBuildingRoad = unit->isBuilding() && unit->buildingImprovementId.startsWith("road");
|
||
bool hasRoute = unit->hasRoadRoute();
|
||
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildRoad);
|
||
isActive = isMode || isBuildingRoad || hasRoute || isQueued;
|
||
iconFile = "images/actions/road.png";
|
||
if (isBuildingRoad) {
|
||
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
|
||
label = Localization::instance().get("ui.unit.btn_road_building", "Building %1%").arg(pct);
|
||
tip = Localization::instance().get("ui.unit.tip_road_building", "Busy building road... %1%").arg(pct);
|
||
} else if (hasRoute) {
|
||
int cur = (int)unit->roadRouteIndex + 1;
|
||
int total = (int)unit->roadRoute.size();
|
||
label = Localization::instance().get("ui.unit.btn_road_route", "Route (%1/%2)").arg(cur).arg(total);
|
||
tip = Localization::instance().get("ui.unit.status_road_route", "Road Route: %1/%2").arg(cur).arg(total);
|
||
} else if (isQueued) {
|
||
label = Localization::instance().get("ui.unit.btn_road_queued", "Road (Queued)");
|
||
tip = Localization::instance().get("ui.unit.tip_road_queued", "Build Road (Queued on arrival - click to cancel)");
|
||
} else {
|
||
label = Localization::instance().get("ui.unit.btn_build_road", "Road [R]");
|
||
tip = Localization::instance().get("ui.unit.tip_build_road", "Build Road (R) - Click target tile to build route");
|
||
}
|
||
}
|
||
|
||
// Circular bubble button
|
||
painter.setBrush(isActive ? QColor(45, 85, 145, 250) : QColor(24, 32, 46, 220));
|
||
painter.setPen(QPen(isActive ? QColor(255, 220, 50) : QColor(80, 110, 150), isActive ? 1.8 : 1.0));
|
||
painter.drawRoundedRect(bRect, 6, 6);
|
||
|
||
// Icon inside button
|
||
QImage aIcon = getTintedIcon(iconFile, isActive ? QColor(255, 230, 80) : primaryCol);
|
||
painter.drawImage(QRect(bRect.left() + 8, bRect.top() + 10, 20, 20), aIcon);
|
||
|
||
// Label
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(isActive ? QColor(255, 235, 90) : QColor(210, 225, 245));
|
||
painter.drawText(QRect(bRect.left() + 32, bRect.top(), bRect.width() - 36, bRect.height()), Qt::AlignLeft | Qt::AlignVCenter, label);
|
||
|
||
m_activeActionBubbles.push_back({bRect, act, tip});
|
||
}
|
||
}
|
||
|
||
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);
|
||
|
||
for (const auto &t : city.territory) {
|
||
int tx = t.first;
|
||
int ty = t.second;
|
||
QVector2D center = Pathfinder::hexToWorld(tx, ty);
|
||
|
||
for (int e = 0; e < 6; ++e) {
|
||
int nx, ny;
|
||
getHexNeighbor(tx, ty, e, nx, ny);
|
||
|
||
if (!city.ownsTile(nx, ny)) {
|
||
QVector2D p1, p2;
|
||
getHexEdgeEndpoints(e, p1, p2);
|
||
|
||
float wx1 = center.x() + p1.x();
|
||
float wy1 = center.y() + p1.y();
|
||
float wz1 = Unit::getTerrainHeight(wx1, wy1, m_point, m_numPoints05) + 0.035f;
|
||
|
||
float wx2 = center.x() + p2.x();
|
||
float wy2 = center.y() + p2.y();
|
||
float wz2 = Unit::getTerrainHeight(wx2, wy2, m_point, m_numPoints05) + 0.035f;
|
||
|
||
bool inFront1 = false, inFront2 = false;
|
||
QPointF s1 = UnitRenderer::projectToScreen(QVector3D(wx1, wy1, wz1), Projection, vMatrix, width(), height(), inFront1);
|
||
QPointF s2 = UnitRenderer::projectToScreen(QVector3D(wx2, wy2, wz2), Projection, vMatrix, width(), height(), inFront2);
|
||
|
||
if (inFront1 && inFront2) {
|
||
QColor glow = ribbonColor;
|
||
glow.setAlpha(90);
|
||
painter.setPen(QPen(glow, 5.5f, Qt::SolidLine, Qt::RoundCap));
|
||
painter.drawLine(s1, s2);
|
||
|
||
painter.setPen(QPen(ribbonColor, 2.6f, Qt::SolidLine, Qt::RoundCap));
|
||
painter.drawLine(s1, s2);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::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, Localization::instance().get("ui.improvement.farm", "Farm"));
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
|
||
{
|
||
m_citySlotHits.clear();
|
||
m_cityBuyTileHits.clear();
|
||
m_cityProjectHits.clear();
|
||
m_cityQueueItemHits.clear();
|
||
m_cancelProjectBtnRect = QRect();
|
||
m_tabBuildingsRect = QRect();
|
||
m_tabUnitsRect = QRect();
|
||
City *city = GameManager::instance().getActiveCityView();
|
||
if (!city) return;
|
||
|
||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||
|
||
const Player *player = GameManager::instance().players().getPlayer(city->playerId);
|
||
QColor primaryCol = player ? player->primaryColor : Qt::blue;
|
||
QColor secondaryCol = player ? player->secondaryColor : Qt::white;
|
||
|
||
int maxRadius = TerrainYieldManager::instance().getCityConfig().maxWorkableRadius; // 3 deep hexagons
|
||
|
||
// 1. Render all hexes up to 3 distance around the city
|
||
for (int dx = -maxRadius - 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;
|
||
|
||
int dist = Pathfinder::hexDistance(city->tileX, city->tileY, hx, hy);
|
||
if (dist > maxRadius) continue;
|
||
|
||
QVector2D center = Pathfinder::hexToWorld(hx, hy);
|
||
float gz = Unit::getTerrainHeight(center.x(), center.y(), m_point, m_numPoints05);
|
||
bool inFront = false;
|
||
QPointF sCenter = UnitRenderer::projectToScreen(QVector3D(center.x(), center.y(), gz + 0.05f), Projection, vMatrix, width(), height(), inFront);
|
||
if (!inFront) continue;
|
||
|
||
bool isOwned = city->ownsTile(hx, hy);
|
||
|
||
// Draw hexagon boundary outline
|
||
QPolygonF hexPoly;
|
||
bool polyValid = true;
|
||
for (int e = 0; e < 6; ++e) {
|
||
QVector2D p1, p2;
|
||
getHexEdgeEndpoints(e, p1, p2);
|
||
float px = center.x() + p1.x();
|
||
float py = center.y() + p1.y();
|
||
float pz = Unit::getTerrainHeight(px, py, m_point, m_numPoints05) + 0.02f;
|
||
bool pInFront = false;
|
||
QPointF sp = UnitRenderer::projectToScreen(QVector3D(px, py, pz), Projection, vMatrix, width(), height(), pInFront);
|
||
if (!pInFront) { polyValid = false; break; }
|
||
hexPoly << sp;
|
||
}
|
||
|
||
if (polyValid) {
|
||
if (isOwned) {
|
||
painter.setPen(QPen(QColor(80, 200, 255, 140), 1.4, Qt::SolidLine));
|
||
painter.setBrush(QColor(40, 120, 200, 30));
|
||
} else {
|
||
painter.setPen(QPen(QColor(120, 130, 145, 90), 1.0, Qt::DashLine));
|
||
painter.setBrush(QColor(20, 25, 35, 40));
|
||
}
|
||
painter.drawPolygon(hexPoly);
|
||
}
|
||
|
||
// Check workability
|
||
QString tType = TerrainYieldManager::instance().getTerrainType(hx, hy, m_point, m_numPoints05);
|
||
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
|
||
|
||
if (isOwned) {
|
||
if (yCfg && yCfg->workable) {
|
||
// Tile has 1 slot!
|
||
bool isWorked = city->isTileWorked(hx, hy);
|
||
QString bestRes = GameManager::instance().getBestTileResource(hx, hy, m_point, m_numPoints05);
|
||
QString res = city->getEffectiveResource(hx, hy, bestRes);
|
||
int amount = GameManager::instance().getTileYield(hx, hy, res, m_point, m_numPoints05);
|
||
const TileImprovement *imp = GameManager::instance().getImprovementAt(hx, hy);
|
||
|
||
float slotR = 15.0f;
|
||
QRect slotRect((int)(sCenter.x() - slotR), (int)(sCenter.y() - slotR), (int)(slotR * 2), (int)(slotR * 2));
|
||
m_citySlotHits.push_back({slotRect, hx, hy, res});
|
||
|
||
// Slot circle background
|
||
painter.setBrush(isWorked ? QColor(25, 55, 38, 245) : QColor(18, 25, 36, 225));
|
||
painter.setPen(QPen(isWorked ? QColor(80, 240, 120) : QColor(140, 175, 215), isWorked ? 2.2 : 1.2));
|
||
painter.drawEllipse(sCenter, slotR, slotR);
|
||
|
||
// Long-press charging progress ring on the active held slot
|
||
if (m_slotPressPending && m_pressedSlotTx == hx && m_pressedSlotTy == hy) {
|
||
int dur = TerrainYieldManager::instance().getCityConfig().slotLongPressDurationMs;
|
||
float charge = std::min(1.0f, (float)m_slotPressElapsed.elapsed() / (float)std::max(1, dur));
|
||
QRectF chargeRect(sCenter.x() - (slotR + 3.0f), sCenter.y() - (slotR + 3.0f), (slotR + 3.0f) * 2, (slotR + 3.0f) * 2);
|
||
painter.setPen(QPen(QColor(255, 220, 80, 230), 2.5));
|
||
painter.setBrush(Qt::NoBrush);
|
||
painter.drawArc(chargeRect, 90 * 16, -(int)(charge * 360 * 16));
|
||
}
|
||
|
||
// Active menu highlight ring
|
||
if (m_resourceMenuOpen && m_resourceMenuTx == hx && m_resourceMenuTy == hy) {
|
||
QRectF activeMenuRect(sCenter.x() - (slotR + 3.0f), sCenter.y() - (slotR + 3.0f), (slotR + 3.0f) * 2, (slotR + 3.0f) * 2);
|
||
painter.setPen(QPen(QColor(80, 200, 255, 240), 2.0));
|
||
painter.setBrush(Qt::NoBrush);
|
||
painter.drawEllipse(activeMenuRect);
|
||
}
|
||
|
||
// Resource icon
|
||
QImage rIcon = getTintedIcon("images/resources/" + res + ".png", isWorked ? QColor(80, 255, 120) : QColor(180, 210, 245));
|
||
painter.drawImage(QRectF(sCenter.x() - 8.5, sCenter.y() - 8.5, 17, 17), rIcon);
|
||
|
||
// 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));
|
||
painter.setPen(QPen(isWorked ? QColor(80, 240, 120, 160) : QColor(80, 110, 140, 120), 1.0));
|
||
painter.drawRoundedRect(pillRect, 3, 3);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(isWorked ? QColor(100, 255, 140) : QColor(180, 205, 235));
|
||
painter.drawText(pillRect, Qt::AlignCenter, QString("+%1").arg(amount));
|
||
|
||
// If unworked and pop is available: show small green +
|
||
if (!isWorked && city->getAvailablePop() > 0) {
|
||
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.008f) + 1.0f) * 0.5f;
|
||
painter.setBrush(QColor(60, 220, 90, (int)(180 + pulse * 75)));
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawEllipse(QPointF(sCenter.x() + 10, sCenter.y() - 10), 4.5, 4.5);
|
||
}
|
||
}
|
||
// If not workable (e.g. Sneeuw / Snow): no slot is shown, as requested!
|
||
} else {
|
||
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);
|
||
|
||
// "Buy" 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,
|
||
Localization::instance().get("ui.city.buy_tile", "BUY"));
|
||
} 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,
|
||
Localization::instance().get("ui.city.occupied", "OCCUPIED"));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. City Management Top Card
|
||
int panelW = 340;
|
||
int panelX = width() - panelW - 14;
|
||
int availableCardW = std::max(360, panelX - 28);
|
||
int cardW = std::min(600, availableCardW);
|
||
int cardH = 174;
|
||
int cardX = 14 + (panelX - 14 - cardW) / 2;
|
||
QRect cardRect(cardX, 14, cardW, cardH);
|
||
|
||
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));
|
||
QString titleText = Localization::instance().get("ui.city.management_title", "%1 - CITY MANAGEMENT").arg(city->name.toUpper());
|
||
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 8, 380, 24), Qt::AlignLeft | Qt::AlignVCenter, titleText);
|
||
|
||
// Close button [X]
|
||
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);
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(220, 235, 255));
|
||
painter.drawText(m_closeCityViewBtnRect, Qt::AlignCenter, Localization::instance().get("ui.city.close_btn", "[X] Close"));
|
||
|
||
// 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", 8));
|
||
painter.setPen(QColor(180, 205, 235));
|
||
QString popInfo = Localization::instance().get("ui.city.local_info", "Local: Population: %L1 (%2 Pop: %3 working, %4 free) | Labor: %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 = Localization::instance().get("ui.city.national_info", "National: Culture: %L1 | Science: %L2 | Gold: %L3 | Faith: %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() + 72, cardRect.right() - 16, cardRect.top() + 72);
|
||
|
||
// Row 3: Total Yields Summary Row (Base 500 + worked tiles + buildings)
|
||
CityYieldSummary summary = GameManager::instance().calculateCityYields(*city, m_point, m_numPoints05);
|
||
|
||
struct YieldItem { QString name; int val; QString icon; QColor col; };
|
||
std::vector<YieldItem> yields = {
|
||
{"Food", summary.food, "images/resources/voedsel.png", QColor(100, 230, 110)},
|
||
{"Labor", summary.labor, "images/resources/arbeid.png", QColor(240, 140, 50)},
|
||
{"Science", summary.science, "images/resources/kennis.png", QColor(60, 190, 255)},
|
||
{"Culture", summary.culture, "images/resources/cultuur.png", QColor(200, 110, 240)},
|
||
{"Faith", summary.faith, "images/resources/geloof.png", QColor(255, 230, 80)},
|
||
{"Gold", summary.gold, "images/resources/goud.png", QColor(255, 210, 50)}
|
||
};
|
||
|
||
int colW = (cardRect.width() - 32) / 6;
|
||
for (size_t i = 0; i < yields.size(); ++i) {
|
||
int cx = cardRect.left() + 16 + (int)i * colW;
|
||
QRect yRect(cx, cardRect.top() + 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() + 5, 16, 16), icon);
|
||
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(160, 180, 205));
|
||
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", 8, QFont::Bold));
|
||
painter.setPen(yields[i].col);
|
||
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 of top card
|
||
painter.setFont(QFont("sans-serif", 7, QFont::StyleItalic));
|
||
painter.setPen(QColor(140, 170, 200));
|
||
painter.drawText(QRect(cardRect.left() + 16, cardRect.bottom() - 18, cardRect.width() - 32, 16), Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.help_text", "Click a tile to assign 1 Pop | Click an unowned tile to buy with Culture."));
|
||
|
||
// 3. City Production & Queue Vertical Panel (Right Side)
|
||
int panelH = height() - 28;
|
||
int panelY = 14;
|
||
QRect prodPanelRect(panelX, panelY, panelW, panelH);
|
||
|
||
painter.setBrush(QColor(16, 22, 32, 248));
|
||
painter.setPen(QPen(primaryCol, 1.8));
|
||
painter.drawRoundedRect(prodPanelRect, 10, 10);
|
||
|
||
int curY = prodPanelRect.top() + 10;
|
||
int innerX = prodPanelRect.left() + 12;
|
||
int innerW = prodPanelRect.width() - 24;
|
||
|
||
const CityConfig &cityCfg = TerrainYieldManager::instance().getCityConfig();
|
||
int maxQueue = cityCfg.maxProductionQueueSize;
|
||
|
||
// Header: City Production Title
|
||
painter.setFont(QFont("sans-serif", 9, QFont::Bold));
|
||
painter.setPen(QColor(230, 240, 255));
|
||
painter.drawText(QRect(innerX, curY, innerW - 80, 18),
|
||
Qt::AlignLeft | Qt::AlignVCenter,
|
||
Localization::instance().get("ui.city.production_panel_title", "CITY PRODUCTION"));
|
||
|
||
// Queue count indicator
|
||
int totalQueuedCount = (int)city->projectQueue.size() + (city->hasActiveProject() ? 1 : 0);
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(totalQueuedCount > maxQueue ? QColor(255, 120, 120) : QColor(140, 185, 225));
|
||
painter.drawText(QRect(innerX + innerW - 80, curY, 80, 18),
|
||
Qt::AlignRight | Qt::AlignVCenter,
|
||
QString("%1 / %2").arg(totalQueuedCount).arg(maxQueue + 1));
|
||
curY += 22;
|
||
|
||
// Section 1: Active Production Header
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(220, 200, 140));
|
||
painter.drawText(QRect(innerX, curY, innerW, 16),
|
||
Qt::AlignLeft | Qt::AlignVCenter,
|
||
Localization::instance().get("ui.city.current_production", "CURRENT PRODUCTION"));
|
||
curY += 18;
|
||
|
||
if (city->hasActiveProject()) {
|
||
const CityProject &proj = city->currentProject;
|
||
float progress = proj.getProgress();
|
||
int pct = (int)(progress * 100.0f);
|
||
int remainingLabor = proj.getRemainingLabor();
|
||
int laborRate = std::max(1, summary.labor);
|
||
int estTicks = (int)std::ceil((float)remainingLabor / (float)laborRate);
|
||
|
||
QRect activeBox(innerX, curY, innerW, 82);
|
||
painter.setBrush(QColor(24, 34, 48, 220));
|
||
painter.setPen(QPen(QColor(255, 215, 60, 180), 1.2));
|
||
painter.drawRoundedRect(activeBox, 6, 6);
|
||
|
||
// Project Icon
|
||
QString projIconPath = "images/improvements/granary.png";
|
||
if (proj.type == CityProjectType::Unit) {
|
||
const UnitConfig *uCfg = GameManager::instance().unitConfigs().getConfig(proj.id);
|
||
if (uCfg && !uCfg->iconPath.isEmpty()) projIconPath = uCfg->iconPath;
|
||
} else if (proj.type == CityProjectType::Building) {
|
||
const BuildingConfig *bCfg = GameManager::instance().buildingConfigs().getBuilding(proj.id);
|
||
if (bCfg && !bCfg->iconPath.isEmpty()) projIconPath = bCfg->iconPath;
|
||
}
|
||
QImage pIcon = getTintedIcon(projIconPath, QColor(255, 215, 60));
|
||
painter.drawImage(QRect(activeBox.left() + 8, activeBox.top() + 8, 26, 26), pIcon);
|
||
|
||
// Name
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(255, 235, 120));
|
||
painter.drawText(QRect(activeBox.left() + 38, activeBox.top() + 6, activeBox.width() - 105, 16),
|
||
Qt::AlignLeft | Qt::AlignVCenter, proj.name);
|
||
|
||
// District / Type
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(170, 195, 220));
|
||
QString typeDesc = (proj.type == CityProjectType::Unit)
|
||
? Localization::instance().get("ui.city.tab_units", "Unit")
|
||
: QString("Building (%1)").arg(proj.district);
|
||
painter.drawText(QRect(activeBox.left() + 38, activeBox.top() + 21, activeBox.width() - 105, 13),
|
||
Qt::AlignLeft | Qt::AlignVCenter, typeDesc);
|
||
|
||
// Cancel button
|
||
m_cancelProjectBtnRect = QRect(activeBox.right() - 62, activeBox.top() + 7, 56, 22);
|
||
painter.setBrush(QColor(50, 25, 30, 220));
|
||
painter.setPen(QPen(QColor(220, 90, 90), 1.0));
|
||
painter.drawRoundedRect(m_cancelProjectBtnRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(255, 150, 150));
|
||
painter.drawText(m_cancelProjectBtnRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.cancel_btn", "Cancel"));
|
||
|
||
// Progress Bar
|
||
QRect barBg(activeBox.left() + 8, activeBox.top() + 40, activeBox.width() - 16, 15);
|
||
painter.setBrush(QColor(18, 25, 36));
|
||
painter.setPen(QPen(QColor(55, 80, 115), 1.0));
|
||
painter.drawRoundedRect(barBg, 3, 3);
|
||
|
||
QRect barFg(activeBox.left() + 8, activeBox.top() + 40, (int)((activeBox.width() - 16) * progress), 15);
|
||
painter.setBrush(QColor(240, 145, 40));
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawRoundedRect(barFg, 3, 3);
|
||
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(Qt::white);
|
||
QString progText = QString("%1% (%L2 / %L3 Labor)").arg(pct).arg(proj.currentLabor).arg(proj.laborCost);
|
||
painter.drawText(barBg, Qt::AlignCenter, progText);
|
||
|
||
// Subtitle: Estimated ticks remaining
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(160, 185, 215));
|
||
painter.drawText(QRect(activeBox.left() + 8, activeBox.top() + 59, activeBox.width() - 16, 14),
|
||
Qt::AlignLeft | Qt::AlignVCenter,
|
||
QString("~%1 %2 remaining (+%3 Labor/tick)")
|
||
.arg(estTicks).arg(estTicks == 1 ? "tick" : "ticks").arg(laborRate));
|
||
|
||
curY += 88;
|
||
} else {
|
||
// No active project
|
||
QRect noActiveBox(innerX, curY, innerW, 44);
|
||
painter.setBrush(QColor(20, 26, 36, 180));
|
||
painter.setPen(QPen(QColor(50, 65, 85), 1.0));
|
||
painter.drawRoundedRect(noActiveBox, 6, 6);
|
||
|
||
painter.setFont(QFont("sans-serif", 8, QFont::StyleItalic));
|
||
painter.setPen(QColor(180, 195, 210));
|
||
painter.drawText(QRect(noActiveBox.left() + 10, noActiveBox.top() + 6, noActiveBox.width() - 20, 16),
|
||
Qt::AlignLeft | Qt::AlignVCenter,
|
||
Localization::instance().get("ui.city.no_project", "No active project"));
|
||
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(140, 160, 180));
|
||
painter.drawText(QRect(noActiveBox.left() + 10, noActiveBox.top() + 22, noActiveBox.width() - 20, 16),
|
||
Qt::AlignLeft | Qt::AlignVCenter,
|
||
Localization::instance().get("ui.city.select_prompt", "Select an item below to construct"));
|
||
curY += 50;
|
||
}
|
||
|
||
// Section 2: Queue Header
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(180, 205, 235));
|
||
QString qHeader = Localization::instance().get("ui.city.queue_header", "QUEUE (%1 / %2)")
|
||
.arg(city->projectQueue.size()).arg(maxQueue);
|
||
painter.drawText(QRect(innerX, curY, innerW, 16), Qt::AlignLeft | Qt::AlignVCenter, qHeader);
|
||
curY += 18;
|
||
|
||
if (city->projectQueue.empty()) {
|
||
QRect emptyQBox(innerX, curY, innerW, 26);
|
||
painter.setBrush(QColor(18, 24, 34, 140));
|
||
painter.setPen(QPen(QColor(40, 55, 75, 100), 1.0, Qt::DashLine));
|
||
painter.drawRoundedRect(emptyQBox, 4, 4);
|
||
|
||
painter.setFont(QFont("sans-serif", 7, QFont::StyleItalic));
|
||
painter.setPen(QColor(130, 150, 175));
|
||
painter.drawText(emptyQBox, Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.queue_empty", "Queue is empty"));
|
||
curY += 32;
|
||
} else {
|
||
for (size_t qi = 0; qi < city->projectQueue.size(); ++qi) {
|
||
const CityProject &qProj = city->projectQueue[qi];
|
||
QRect qRow(innerX, curY, innerW, 28);
|
||
|
||
painter.setBrush(QColor(22, 30, 42, 210));
|
||
painter.setPen(QPen(QColor(50, 70, 95), 1.0));
|
||
painter.drawRoundedRect(qRow, 4, 4);
|
||
|
||
// Badge #1, #2
|
||
QRect numBadge(qRow.left() + 4, qRow.top() + 4, 22, 20);
|
||
painter.setBrush(QColor(40, 60, 90));
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawRoundedRect(numBadge, 3, 3);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(180, 220, 255));
|
||
painter.drawText(numBadge, Qt::AlignCenter, QString("#%1").arg(qi + 1));
|
||
|
||
// Icon
|
||
QString qIconPath = "images/improvements/granary.png";
|
||
if (qProj.type == CityProjectType::Unit) {
|
||
const UnitConfig *uCfg = GameManager::instance().unitConfigs().getConfig(qProj.id);
|
||
if (uCfg && !uCfg->iconPath.isEmpty()) qIconPath = uCfg->iconPath;
|
||
} else if (qProj.type == CityProjectType::Building) {
|
||
const BuildingConfig *bCfg = GameManager::instance().buildingConfigs().getBuilding(qProj.id);
|
||
if (bCfg && !bCfg->iconPath.isEmpty()) qIconPath = bCfg->iconPath;
|
||
}
|
||
QImage qIcon = getTintedIcon(qIconPath, QColor(255, 215, 60));
|
||
painter.drawImage(QRect(qRow.left() + 30, qRow.top() + 5, 18, 18), qIcon);
|
||
|
||
// Name
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(230, 240, 255));
|
||
painter.drawText(QRect(qRow.left() + 52, qRow.top() + 2, innerW - 138, 24),
|
||
Qt::AlignLeft | Qt::AlignVCenter, qProj.name);
|
||
|
||
// Labor Cost
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(240, 160, 50));
|
||
painter.drawText(QRect(qRow.right() - 84, qRow.top() + 2, 54, 24),
|
||
Qt::AlignRight | Qt::AlignVCenter, QString("%1 L").arg(qProj.laborCost));
|
||
|
||
// Remove Button [X]
|
||
QRect remRect(qRow.right() - 24, qRow.top() + 4, 20, 20);
|
||
painter.setBrush(QColor(50, 25, 30, 200));
|
||
painter.setPen(QPen(QColor(200, 80, 80), 1.0));
|
||
painter.drawRoundedRect(remRect, 3, 3);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(255, 140, 140));
|
||
painter.drawText(remRect, Qt::AlignCenter, "X");
|
||
|
||
m_cityQueueItemHits.push_back({remRect, (int)qi});
|
||
curY += 32;
|
||
}
|
||
}
|
||
|
||
// Section 3: Districts Header
|
||
painter.setPen(QPen(QColor(60, 85, 120, 150), 1.0));
|
||
painter.drawLine(innerX, curY + 2, innerX + innerW, curY + 2);
|
||
curY += 8;
|
||
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(220, 200, 140));
|
||
painter.drawText(QRect(innerX, curY, innerW, 16),
|
||
Qt::AlignLeft | Qt::AlignVCenter,
|
||
Localization::instance().get("ui.city.districts_header", "DISTRICT: CITY CENTER"));
|
||
curY += 18;
|
||
|
||
if (!city->completedBuildings.empty()) {
|
||
for (const QString &bId : city->completedBuildings) {
|
||
const BuildingConfig *bCfg = GameManager::instance().buildingConfigs().getBuilding(bId);
|
||
if (!bCfg) continue;
|
||
|
||
QRect bBadge(innerX, curY, innerW, 28);
|
||
painter.setBrush(QColor(22, 38, 28, 210));
|
||
painter.setPen(QPen(QColor(80, 220, 120), 1.0));
|
||
painter.drawRoundedRect(bBadge, 4, 4);
|
||
|
||
QImage bIcon = getTintedIcon(bCfg->iconPath.isEmpty() ? "images/improvements/granary.png" : bCfg->iconPath, QColor(80, 240, 120));
|
||
painter.drawImage(QRect(bBadge.left() + 6, bBadge.top() + 5, 18, 18), bIcon);
|
||
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(210, 255, 220));
|
||
painter.drawText(QRect(bBadge.left() + 28, bBadge.top() + 2, 100, 24),
|
||
Qt::AlignLeft | Qt::AlignVCenter, bCfg->name);
|
||
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(160, 230, 180));
|
||
painter.drawText(QRect(bBadge.left() + 132, bBadge.top() + 2, bBadge.width() - 136, 24),
|
||
Qt::AlignLeft | Qt::AlignVCenter, bCfg->description);
|
||
curY += 32;
|
||
}
|
||
} else {
|
||
painter.setFont(QFont("sans-serif", 7, QFont::StyleItalic));
|
||
painter.setPen(QColor(140, 160, 180));
|
||
painter.drawText(QRect(innerX, curY, innerW, 16),
|
||
Qt::AlignLeft | Qt::AlignVCenter,
|
||
Localization::instance().get("ui.city.no_buildings", "No extensions built yet"));
|
||
curY += 20;
|
||
}
|
||
|
||
// Section 4: Projects Section Divider & Tabs
|
||
painter.setPen(QPen(QColor(60, 85, 120, 150), 1.0));
|
||
painter.drawLine(innerX, curY + 2, innerX + innerW, curY + 2);
|
||
curY += 8;
|
||
|
||
// Tabs: Buildings & Units
|
||
int tabW = (innerW - 8) / 2;
|
||
int tabH = 22;
|
||
m_tabBuildingsRect = QRect(innerX, curY, tabW, tabH);
|
||
m_tabUnitsRect = QRect(m_tabBuildingsRect.right() + 8, curY, tabW, tabH);
|
||
|
||
bool isBTab = (m_cityProjectTab == 0);
|
||
painter.setBrush(isBTab ? QColor(45, 75, 120, 240) : QColor(25, 35, 50, 180));
|
||
painter.setPen(QPen(isBTab ? QColor(100, 190, 255) : QColor(60, 85, 120), isBTab ? 1.5 : 1.0));
|
||
painter.drawRoundedRect(m_tabBuildingsRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 8, isBTab ? QFont::Bold : QFont::Normal));
|
||
painter.setPen(isBTab ? Qt::white : QColor(160, 185, 215));
|
||
painter.drawText(m_tabBuildingsRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.tab_buildings", "Buildings"));
|
||
|
||
bool isUTab = (m_cityProjectTab == 1);
|
||
painter.setBrush(isUTab ? QColor(45, 75, 120, 240) : QColor(25, 35, 50, 180));
|
||
painter.setPen(QPen(isUTab ? QColor(100, 190, 255) : QColor(60, 85, 120), isUTab ? 1.5 : 1.0));
|
||
painter.drawRoundedRect(m_tabUnitsRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 8, isUTab ? QFont::Bold : QFont::Normal));
|
||
painter.setPen(isUTab ? Qt::white : QColor(160, 185, 215));
|
||
painter.drawText(m_tabUnitsRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.tab_units", "Units"));
|
||
curY += 28;
|
||
|
||
// List of available projects under selected tab
|
||
int itemH = 50;
|
||
if (m_cityProjectTab == 0) {
|
||
// Buildings tab (only show unlocked, non-obsolete buildings)
|
||
const auto &buildings = GameManager::instance().buildingConfigs().getAllBuildings();
|
||
for (auto it = buildings.begin(); it != buildings.end(); ++it) {
|
||
if (curY + itemH > prodPanelRect.bottom() - 6) break;
|
||
|
||
const BuildingConfig &bCfg = it.value();
|
||
if (player) {
|
||
if (!player->isItemUnlocked(bCfg.requiredTech)) continue;
|
||
if (player->isItemObsolete(bCfg.obsoleteTech)) continue;
|
||
}
|
||
QRect itemRect(innerX, curY, innerW, itemH);
|
||
|
||
bool alreadyBuilt = city->hasBuilding(bCfg.id);
|
||
bool isActiveProj = (city->hasActiveProject() && city->currentProject.type == CityProjectType::Building && city->currentProject.id == bCfg.id);
|
||
int qPos = city->getQueuedBuildingPosition(bCfg.id);
|
||
bool isQueued = (qPos > 0);
|
||
bool isQueueFull = ((int)city->projectQueue.size() >= maxQueue && city->hasActiveProject());
|
||
|
||
painter.setBrush(isActiveProj ? QColor(35, 50, 75, 230)
|
||
: (isQueued ? QColor(26, 40, 58, 220)
|
||
: (alreadyBuilt ? QColor(20, 25, 32, 190) : QColor(24, 32, 46, 220))));
|
||
painter.setPen(QPen(isActiveProj ? QColor(255, 220, 50)
|
||
: (isQueued ? QColor(100, 190, 255)
|
||
: (alreadyBuilt ? QColor(60, 75, 95) : QColor(70, 100, 140))),
|
||
(isActiveProj || isQueued) ? 1.5 : 1.0));
|
||
painter.drawRoundedRect(itemRect, 6, 6);
|
||
|
||
// Icon
|
||
QImage bIcon = getTintedIcon(bCfg.iconPath.isEmpty() ? "images/improvements/granary.png" : bCfg.iconPath,
|
||
alreadyBuilt ? QColor(120, 140, 160) : QColor(255, 215, 60));
|
||
painter.drawImage(QRect(itemRect.left() + 6, itemRect.top() + 7, 24, 24), bIcon);
|
||
|
||
// Name
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(alreadyBuilt ? QColor(150, 165, 180) : QColor(245, 250, 255));
|
||
painter.drawText(QRect(itemRect.left() + 36, itemRect.top() + 4, itemRect.width() - 105, 15),
|
||
Qt::AlignLeft | Qt::AlignVCenter, bCfg.name);
|
||
|
||
// Cost & estimated ticks
|
||
int estTicks = (int)std::ceil((float)bCfg.laborCost / (float)std::max(1, summary.labor));
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(240, 160, 50));
|
||
QString costStr = QString("%L1 Labor (~%2 %3)").arg(bCfg.laborCost).arg(estTicks).arg(estTicks == 1 ? "tick" : "ticks");
|
||
painter.drawText(QRect(itemRect.left() + 36, itemRect.top() + 19, itemRect.width() - 105, 13),
|
||
Qt::AlignLeft | Qt::AlignVCenter, costStr);
|
||
|
||
// Description / Effect
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(alreadyBuilt ? QColor(120, 180, 140) : QColor(100, 240, 130));
|
||
painter.drawText(QRect(itemRect.left() + 36, itemRect.top() + 32, itemRect.width() - 40, 14),
|
||
Qt::AlignLeft | Qt::AlignVCenter, bCfg.description);
|
||
|
||
// Action Button / Status badge on right side
|
||
QRect btnRect(itemRect.right() - 66, itemRect.top() + 12, 60, 26);
|
||
if (alreadyBuilt) {
|
||
painter.setBrush(QColor(30, 40, 35, 180));
|
||
painter.setPen(QPen(QColor(70, 120, 85), 1.0));
|
||
painter.drawRoundedRect(btnRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(120, 180, 130));
|
||
painter.drawText(btnRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.status_built", "BUILT"));
|
||
} else if (isActiveProj) {
|
||
painter.setBrush(QColor(60, 50, 20, 220));
|
||
painter.setPen(QPen(QColor(255, 215, 50), 1.2));
|
||
painter.drawRoundedRect(btnRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(255, 230, 80));
|
||
painter.drawText(btnRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.status_in_progress", "ACTIVE"));
|
||
} else if (isQueued) {
|
||
painter.setBrush(QColor(30, 50, 75, 220));
|
||
painter.setPen(QPen(QColor(80, 180, 255), 1.2));
|
||
painter.drawRoundedRect(btnRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(140, 215, 255));
|
||
painter.drawText(btnRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.status_queued", "QUEUED #%1").arg(qPos));
|
||
} else if (isQueueFull) {
|
||
painter.setBrush(QColor(40, 30, 35, 180));
|
||
painter.setPen(QPen(QColor(120, 70, 75), 1.0));
|
||
painter.drawRoundedRect(btnRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(180, 130, 135));
|
||
painter.drawText(btnRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.status_full", "FULL"));
|
||
} else {
|
||
bool hasAct = city->hasActiveProject();
|
||
painter.setBrush(hasAct ? QColor(25, 55, 90, 240) : QColor(30, 60, 100, 240));
|
||
painter.setPen(QPen(hasAct ? QColor(60, 175, 245) : QColor(80, 160, 255), 1.2));
|
||
painter.drawRoundedRect(btnRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(Qt::white);
|
||
QString btnLabel = hasAct
|
||
? Localization::instance().get("ui.city.btn_queue", "+ Queue")
|
||
: Localization::instance().get("ui.city.btn_build", "Build");
|
||
painter.drawText(btnRect, Qt::AlignCenter, btnLabel);
|
||
|
||
m_cityProjectHits.push_back({btnRect, CityProjectType::Building, bCfg.id});
|
||
}
|
||
|
||
curY += itemH + 6;
|
||
}
|
||
} else {
|
||
// Units tab (only show unlocked, non-obsolete units)
|
||
const auto &units = GameManager::instance().unitConfigs().getAllConfigs();
|
||
for (auto it = units.begin(); it != units.end(); ++it) {
|
||
if (curY + itemH > prodPanelRect.bottom() - 6) break;
|
||
|
||
const UnitConfig &uCfg = it.value();
|
||
if (player) {
|
||
if (!player->isItemUnlocked(uCfg.requiredTech)) continue;
|
||
if (player->isItemObsolete(uCfg.obsoleteTech)) continue;
|
||
}
|
||
QRect itemRect(innerX, curY, innerW, itemH);
|
||
|
||
bool isActiveProj = (city->hasActiveProject() && city->currentProject.type == CityProjectType::Unit && city->currentProject.id == uCfg.id);
|
||
int qCount = city->getQueuedUnitCount(uCfg.id);
|
||
bool isQueueFull = ((int)city->projectQueue.size() >= maxQueue && city->hasActiveProject());
|
||
|
||
painter.setBrush(isActiveProj ? QColor(35, 50, 75, 230) : QColor(24, 32, 46, 220));
|
||
painter.setPen(QPen(isActiveProj ? QColor(255, 220, 50) : QColor(70, 100, 140), isActiveProj ? 1.5 : 1.0));
|
||
painter.drawRoundedRect(itemRect, 6, 6);
|
||
|
||
// Icon
|
||
QImage uIcon = getTintedIcon(uCfg.iconPath, primaryCol);
|
||
painter.drawImage(QRect(itemRect.left() + 6, itemRect.top() + 7, 24, 24), uIcon);
|
||
|
||
// Name + Queued Count
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(245, 250, 255));
|
||
QString uName = uCfg.name;
|
||
if (qCount > 0) {
|
||
uName += QString(" (%1 queued)").arg(qCount);
|
||
}
|
||
painter.drawText(QRect(itemRect.left() + 36, itemRect.top() + 4, itemRect.width() - 105, 15),
|
||
Qt::AlignLeft | Qt::AlignVCenter, uName);
|
||
|
||
// Cost & estimated ticks
|
||
int estTicks = (int)std::ceil((float)uCfg.laborCost / (float)std::max(1, summary.labor));
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(240, 160, 50));
|
||
QString costStr = QString("%L1 Labor (~%2 %3)").arg(uCfg.laborCost).arg(estTicks).arg(estTicks == 1 ? "tick" : "ticks");
|
||
painter.drawText(QRect(itemRect.left() + 36, itemRect.top() + 19, itemRect.width() - 105, 13),
|
||
Qt::AlignLeft | Qt::AlignVCenter, costStr);
|
||
|
||
// Stats info
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(160, 185, 215));
|
||
QString infoStr = unitCategoryToString(uCfg.category);
|
||
if (uCfg.isMilitary()) {
|
||
infoStr += QString(" | HP: %1 | Atk: %2").arg(uCfg.health).arg(uCfg.attackDamage);
|
||
}
|
||
painter.drawText(QRect(itemRect.left() + 36, itemRect.top() + 32, itemRect.width() - 40, 14),
|
||
Qt::AlignLeft | Qt::AlignVCenter, infoStr);
|
||
|
||
// Action Button on right side
|
||
QRect btnRect(itemRect.right() - 66, itemRect.top() + 12, 60, 26);
|
||
if (isQueueFull) {
|
||
painter.setBrush(QColor(40, 30, 35, 180));
|
||
painter.setPen(QPen(QColor(120, 70, 75), 1.0));
|
||
painter.drawRoundedRect(btnRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(180, 130, 135));
|
||
painter.drawText(btnRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.city.status_full", "FULL"));
|
||
} else {
|
||
bool hasAct = city->hasActiveProject();
|
||
painter.setBrush(hasAct ? QColor(25, 55, 90, 240) : QColor(30, 60, 100, 240));
|
||
painter.setPen(QPen(hasAct ? QColor(60, 175, 245) : QColor(80, 160, 255), 1.2));
|
||
painter.drawRoundedRect(btnRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(Qt::white);
|
||
QString btnLabel = hasAct
|
||
? Localization::instance().get("ui.city.btn_queue", "+ Queue")
|
||
: Localization::instance().get("ui.city.btn_train", "Train");
|
||
painter.drawText(btnRect, Qt::AlignCenter, btnLabel);
|
||
|
||
m_cityProjectHits.push_back({btnRect, CityProjectType::Unit, uCfg.id});
|
||
}
|
||
|
||
curY += itemH + 6;
|
||
}
|
||
}
|
||
|
||
// 4. Resource Selection Popup Menu (if open)
|
||
if (m_resourceMenuOpen) {
|
||
drawCityResourceMenu(painter, vMatrix);
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::onSlotLongPressTimeout()
|
||
{
|
||
if (m_slotPressPending && GameManager::instance().isInCityView()) {
|
||
m_slotLongPressTriggered = true;
|
||
m_slotPressPending = false;
|
||
m_resourceMenuOpen = true;
|
||
m_resourceMenuTx = m_pressedSlotTx;
|
||
m_resourceMenuTy = m_pressedSlotTy;
|
||
update();
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::drawCityResourceMenu(QPainter &painter, const QMatrix4x4 &vMatrix)
|
||
{
|
||
m_cityResourceChoiceHits.clear();
|
||
m_resourceMenuCloseBtnRect = QRect();
|
||
m_resourceMenuRect = QRect();
|
||
|
||
City *city = GameManager::instance().getActiveCityView();
|
||
if (!city) {
|
||
m_resourceMenuOpen = false;
|
||
return;
|
||
}
|
||
|
||
int tx = m_resourceMenuTx;
|
||
int ty = m_resourceMenuTy;
|
||
if (!city->ownsTile(tx, ty)) {
|
||
m_resourceMenuOpen = false;
|
||
return;
|
||
}
|
||
|
||
QVector2D center = Pathfinder::hexToWorld(tx, ty);
|
||
float gz = Unit::getTerrainHeight(center.x(), center.y(), m_point, m_numPoints05);
|
||
bool inFront = false;
|
||
QPointF sCenter = UnitRenderer::projectToScreen(QVector3D(center.x(), center.y(), gz + 0.05f), Projection, vMatrix, width(), height(), inFront);
|
||
if (!inFront) return;
|
||
|
||
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, m_point, m_numPoints05);
|
||
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
|
||
QString tName = yCfg ? yCfg->name : tType;
|
||
const TileImprovement *imp = GameManager::instance().getImprovementAt(tx, ty);
|
||
if (imp) {
|
||
const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId);
|
||
if (iCfg) {
|
||
tName += QString(" (%1)").arg(iCfg->name);
|
||
}
|
||
}
|
||
|
||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||
QStringList resources = cCfg.resourcePriority;
|
||
if (resources.isEmpty()) {
|
||
resources = QStringList{"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"};
|
||
}
|
||
|
||
QString bestRes = GameManager::instance().getBestTileResource(tx, ty, m_point, m_numPoints05);
|
||
QString activeRes = city->getEffectiveResource(tx, ty, bestRes);
|
||
|
||
int menuW = 240;
|
||
int rowH = 32;
|
||
int headerH = 46;
|
||
int footerH = 10;
|
||
int menuH = headerH + (resources.size() * rowH) + footerH;
|
||
|
||
// Anchor menu near sCenter
|
||
int menuX = (int)sCenter.x() + 24;
|
||
int menuY = (int)sCenter.y() - menuH / 2;
|
||
if (menuX + menuW > width() - 15) {
|
||
menuX = (int)sCenter.x() - menuW - 24;
|
||
}
|
||
if (menuX < 15) menuX = 15;
|
||
if (menuY + menuH > height() - 15) {
|
||
menuY = height() - 15 - menuH;
|
||
}
|
||
if (menuY < 15) menuY = 15;
|
||
|
||
m_resourceMenuRect = QRect(menuX, menuY, menuW, menuH);
|
||
|
||
// Connecting line from slot to menu card
|
||
painter.setPen(QPen(QColor(80, 200, 255, 120), 1.5, Qt::DashLine));
|
||
painter.drawLine(sCenter, QPointF(menuX < sCenter.x() ? menuX + menuW : menuX, menuY + menuH / 2));
|
||
|
||
// Drop shadow
|
||
painter.setBrush(QColor(0, 0, 0, 160));
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawRoundedRect(m_resourceMenuRect.adjusted(4, 4, 4, 4), 8, 8);
|
||
|
||
// Card background
|
||
painter.setBrush(QColor(16, 22, 32, 250));
|
||
painter.setPen(QPen(QColor(80, 180, 240), 1.6));
|
||
painter.drawRoundedRect(m_resourceMenuRect, 8, 8);
|
||
|
||
// Header Title
|
||
painter.setFont(QFont("sans-serif", 9, QFont::Bold));
|
||
painter.setPen(QColor(240, 248, 255));
|
||
QString title = Localization::instance().get("ui.city.select_resource", "SELECT RESOURCE");
|
||
painter.drawText(QRect(menuX + 12, menuY + 6, menuW - 40, 18), Qt::AlignLeft | Qt::AlignVCenter, title);
|
||
|
||
// Close button [X]
|
||
m_resourceMenuCloseBtnRect = QRect(menuX + menuW - 26, menuY + 6, 20, 18);
|
||
painter.setFont(QFont("sans-serif", 9, QFont::Bold));
|
||
painter.setPen(QColor(180, 190, 205));
|
||
painter.drawText(m_resourceMenuCloseBtnRect, Qt::AlignCenter, "×");
|
||
|
||
// Subtitle (tile info)
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(QColor(140, 170, 200));
|
||
painter.drawText(QRect(menuX + 12, menuY + 24, menuW - 24, 16), Qt::AlignLeft | Qt::AlignVCenter, tName);
|
||
|
||
// Divider line
|
||
painter.setPen(QPen(QColor(50, 75, 105), 1.0));
|
||
painter.drawLine(menuX + 10, menuY + 44, menuX + menuW - 10, menuY + 44);
|
||
|
||
// Rows
|
||
int currY = menuY + headerH;
|
||
for (const QString &res : resources) {
|
||
int yieldVal = GameManager::instance().getTileYield(tx, ty, res, m_point, m_numPoints05);
|
||
bool isActive = (res == activeRes);
|
||
bool isBest = (res == bestRes);
|
||
|
||
QRect rowRect(menuX + 8, currY, menuW - 16, rowH - 4);
|
||
m_cityResourceChoiceHits.push_back({rowRect, res, yieldVal});
|
||
|
||
bool isHovered = rowRect.contains(m_mouseCurrentPos);
|
||
|
||
// Row background
|
||
if (isActive) {
|
||
painter.setBrush(QColor(30, 65, 45, 240));
|
||
painter.setPen(QPen(QColor(80, 240, 120), 1.4));
|
||
} else if (isHovered) {
|
||
painter.setBrush(QColor(40, 60, 85, 230));
|
||
painter.setPen(QPen(QColor(100, 190, 255), 1.2));
|
||
} else {
|
||
painter.setBrush(QColor(22, 30, 42, 210));
|
||
painter.setPen(QPen(QColor(45, 60, 80), 1.0));
|
||
}
|
||
painter.drawRoundedRect(rowRect, 5, 5);
|
||
|
||
// Resource icon
|
||
QColor iconColor;
|
||
if (res == "voedsel") iconColor = QColor(90, 255, 130);
|
||
else if (res == "arbeid") iconColor = QColor(255, 160, 60);
|
||
else if (res == "goud") iconColor = QColor(255, 215, 60);
|
||
else if (res == "kennis") iconColor = QColor(80, 180, 255);
|
||
else if (res == "cultuur") iconColor = QColor(225, 110, 255);
|
||
else if (res == "geloof") iconColor = QColor(220, 230, 255);
|
||
else iconColor = QColor(200, 220, 240);
|
||
|
||
QImage resIcon = getTintedIcon("images/resources/" + res + ".png", iconColor);
|
||
painter.drawImage(QRect(rowRect.left() + 6, rowRect.top() + (rowRect.height() - 18) / 2, 18, 18), resIcon);
|
||
|
||
// Resource Name
|
||
QString resName = Localization::instance().get(QString("ui.resources.%1").arg(res), res);
|
||
painter.setFont(QFont("sans-serif", 8, isHovered || isActive ? QFont::Bold : QFont::Normal));
|
||
painter.setPen(isActive ? QColor(100, 255, 150) : (isHovered ? QColor(255, 255, 255) : QColor(200, 215, 230)));
|
||
painter.drawText(QRect(rowRect.left() + 28, rowRect.top(), 80, rowRect.height()), Qt::AlignLeft | Qt::AlignVCenter, resName);
|
||
|
||
// Best tag
|
||
if (isBest) {
|
||
QRect bestRect(rowRect.left() + 110, rowRect.top() + (rowRect.height() - 14) / 2, 34, 14);
|
||
painter.setBrush(QColor(80, 65, 20, 220));
|
||
painter.setPen(QPen(QColor(255, 205, 50), 1.0));
|
||
painter.drawRoundedRect(bestRect, 3, 3);
|
||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||
painter.setPen(QColor(255, 220, 80));
|
||
painter.drawText(bestRect, Qt::AlignCenter, Localization::instance().get("ui.city.best_yield", "BEST"));
|
||
}
|
||
|
||
// Active indicator or yield amount
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(isActive ? QColor(80, 255, 120) : (yieldVal > 0 ? QColor(160, 230, 255) : QColor(130, 145, 160)));
|
||
QString yieldText = QString("+%L1").arg(yieldVal);
|
||
painter.drawText(QRect(rowRect.right() - 70, rowRect.top(), 64, rowRect.height()), Qt::AlignRight | Qt::AlignVCenter, yieldText);
|
||
|
||
currY += rowH;
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
||
{
|
||
// 0. If in City View: handle close button, project tabs/hits, cancel, and slot clicks
|
||
if (GameManager::instance().isInCityView()) {
|
||
City *city = GameManager::instance().getActiveCityView();
|
||
|
||
// 0a. Handle Resource Selection Menu clicks (if open)
|
||
if (m_resourceMenuOpen) {
|
||
if (m_resourceMenuCloseBtnRect.contains(event->pos())) {
|
||
m_resourceMenuOpen = false;
|
||
update();
|
||
return;
|
||
}
|
||
|
||
for (const auto &choice : m_cityResourceChoiceHits) {
|
||
if (choice.rect.contains(event->pos())) {
|
||
if (city) {
|
||
int tx = m_resourceMenuTx;
|
||
int ty = m_resourceMenuTy;
|
||
city->setTilePreferredResource(tx, ty, choice.res);
|
||
|
||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||
float gz = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05);
|
||
QString resName = Localization::instance().get(QString("ui.resources.%1").arg(choice.res), choice.res);
|
||
|
||
if (city->isTileWorked(tx, ty)) {
|
||
// Already working: switch active producing resource
|
||
QString msg = Localization::instance().get("ui.city.resource_switched", "Producing %1 (+%2)").arg(resName).arg(choice.amount);
|
||
GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(80, 255, 120));
|
||
} else {
|
||
if (city->getAvailablePop() > 0) {
|
||
city->assignWorker(tx, ty, choice.res);
|
||
QString msg = Localization::instance().get("ui.city.worker_assigned_res", "+1 Pop: %1 (+%2)").arg(resName).arg(choice.amount);
|
||
GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(80, 255, 120));
|
||
} else {
|
||
QString msg = Localization::instance().get("ui.city.resource_preferred", "Slot set to %1 (+%2)").arg(resName).arg(choice.amount);
|
||
GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 220, 80));
|
||
}
|
||
}
|
||
}
|
||
m_resourceMenuOpen = false;
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Clicked outside resource menu: dismiss popup
|
||
m_resourceMenuOpen = false;
|
||
update();
|
||
return;
|
||
}
|
||
|
||
if (m_closeCityViewBtnRect.contains(event->pos())) {
|
||
m_resourceMenuOpen = false;
|
||
m_slotPressPending = false;
|
||
cameraPosition = m_savedCamPos;
|
||
m_zoom = m_savedZoom;
|
||
zRot = m_savedZRot;
|
||
GameManager::instance().exitCityView();
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// Tab switches
|
||
if (m_tabBuildingsRect.contains(event->pos())) {
|
||
m_cityProjectTab = 0;
|
||
update();
|
||
return;
|
||
}
|
||
if (m_tabUnitsRect.contains(event->pos())) {
|
||
m_cityProjectTab = 1;
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// Cancel active project
|
||
if (city && m_cancelProjectBtnRect.contains(event->pos())) {
|
||
GameManager::instance().cancelCityProject(city->id);
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// Cancel individual queued project item
|
||
if (city) {
|
||
for (const auto &qHit : m_cityQueueItemHits) {
|
||
if (qHit.rect.contains(event->pos())) {
|
||
GameManager::instance().removeQueuedCityProject(city->id, qHit.queueIndex);
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Project selection clicks
|
||
if (city) {
|
||
for (const auto &pHit : m_cityProjectHits) {
|
||
if (pHit.rect.contains(event->pos())) {
|
||
GameManager::instance().queueCityProject(city->id, pHit.type, pHit.id);
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Slot pressed: initiate long press timer for hold-to-change resource
|
||
for (const auto &slot : m_citySlotHits) {
|
||
if (slot.rect.contains(event->pos())) {
|
||
if (city) {
|
||
m_slotPressPending = true;
|
||
m_slotLongPressTriggered = false;
|
||
m_pressedSlotTx = slot.tx;
|
||
m_pressedSlotTy = slot.ty;
|
||
m_slotPressPos = event->pos();
|
||
m_slotPressElapsed.start();
|
||
int dur = TerrainYieldManager::instance().getCityConfig().slotLongPressDurationMs;
|
||
m_slotLongPressTimer->start(std::max(100, dur));
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. Buy tile clicks
|
||
for (const auto &buyHit : m_cityBuyTileHits) {
|
||
if (buyHit.rect.contains(event->pos())) {
|
||
if (city) {
|
||
GameManager::instance().buyTileForCity(city->id, buyHit.tx, buyHit.ty);
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
return; // In City View, ignore all other clicks
|
||
}
|
||
|
||
// 0b. If Tech Tree is open: handle Tech Tree interactions
|
||
if (GameManager::instance().isTechTreeOpen()) {
|
||
if (m_closeTechTreeBtnRect.contains(event->pos()) || m_techTreeBtnRect.contains(event->pos())) {
|
||
m_draggingTechTree = false;
|
||
GameManager::instance().closeTechTree();
|
||
update();
|
||
return;
|
||
}
|
||
if (m_clearTechQueueBtnRect.contains(event->pos()) && event->button() == Qt::LeftButton) {
|
||
GameManager::instance().clearPlayerResearchQueue(1);
|
||
update();
|
||
return;
|
||
}
|
||
// Check queue chips in header
|
||
for (const auto &chip : m_techQueueChipHits) {
|
||
if (chip.rect.contains(event->pos())) {
|
||
if (event->button() == Qt::RightButton || event->button() == Qt::LeftButton) {
|
||
GameManager::instance().removeTechFromPlayerQueue(1, chip.techId);
|
||
update();
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
// Start potential drag or click inside tech tree canvas
|
||
if (m_techTreeCanvasRect.contains(event->pos())) {
|
||
m_draggingTechTree = true;
|
||
m_techTreeDragMoved = false;
|
||
m_techTreeDragStartPos = event->pos();
|
||
lastPos = event->pos();
|
||
return;
|
||
}
|
||
return; // Consume clicks while Tech Tree modal is open
|
||
}
|
||
|
||
// 0c. Top bar Tech Tree button or Science slot click
|
||
if ((m_techTreeBtnRect.contains(event->pos()) || m_scienceBarHitRect.contains(event->pos())) && event->button() == Qt::LeftButton) {
|
||
GameManager::instance().toggleTechTree();
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// Action Queue items (Bottom HUD notification bar)
|
||
for (const auto &hit : m_actionItemHits) {
|
||
if (hit.dismissRect.contains(event->pos()) && event->button() == Qt::LeftButton) {
|
||
GameManager::instance().actionNeededManager().dismissItem(hit.type, hit.entityId);
|
||
update();
|
||
return;
|
||
}
|
||
if (hit.rect.contains(event->pos())) {
|
||
if (event->button() == Qt::RightButton) {
|
||
GameManager::instance().actionNeededManager().dismissItem(hit.type, hit.entityId);
|
||
update();
|
||
return;
|
||
} else if (event->button() == Qt::LeftButton) {
|
||
handleActionItemClick(hit.type, hit.entityId);
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
if (m_actionQueueHeaderRect.contains(event->pos())) {
|
||
return;
|
||
}
|
||
|
||
// 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);
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 1. Zoom Out / Reset / In buttons
|
||
if (getZoomOutButtonRect().contains(event->pos())) {
|
||
m_minimapZoom = std::max(1.0f, m_minimapZoom / 1.5f);
|
||
update();
|
||
return;
|
||
}
|
||
if (getZoomResetButtonRect().contains(event->pos())) {
|
||
m_minimapZoom = 1.0f;
|
||
update();
|
||
return;
|
||
}
|
||
if (getZoomInButtonRect().contains(event->pos())) {
|
||
m_minimapZoom = std::min(8.0f, m_minimapZoom * 1.5f);
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// 2. Minimap click & drag
|
||
QRect mapRect = getMinimapRect();
|
||
if (mapRect.contains(event->pos())) {
|
||
m_draggingMinimap = true;
|
||
QVector2D wPos = minimapToWorld(event->pos(), mapRect);
|
||
cameraPosition.setX(wPos.x());
|
||
cameraPosition.setY(wPos.y());
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// 3. Sim Speed Buttons [1x] [2x] [5x]
|
||
if (m_speed1xRect.contains(event->pos())) {
|
||
GameManager::instance().setSimSpeed(1.0f);
|
||
update();
|
||
return;
|
||
}
|
||
if (m_speed2xRect.contains(event->pos())) {
|
||
GameManager::instance().setSimSpeed(2.0f);
|
||
update();
|
||
return;
|
||
}
|
||
if (m_speed5xRect.contains(event->pos())) {
|
||
GameManager::instance().setSimSpeed(5.0f);
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// 4. Action Bubbles clicked
|
||
for (const auto &ab : m_activeActionBubbles) {
|
||
if (ab.rect.contains(event->pos())) {
|
||
if (ab.actionId == "verplaatsen") {
|
||
if (GameManager::instance().currentActionMode() == ActionMode::Move) {
|
||
GameManager::instance().setActionMode(ActionMode::None);
|
||
} else {
|
||
GameManager::instance().setActionMode(ActionMode::Move);
|
||
}
|
||
} else if (ab.actionId == "aanvallen") {
|
||
if (GameManager::instance().currentActionMode() == ActionMode::Attack) {
|
||
GameManager::instance().setActionMode(ActionMode::None);
|
||
} else {
|
||
GameManager::instance().setActionMode(ActionMode::Attack);
|
||
}
|
||
} 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);
|
||
} else if (ab.actionId == "build_road" || ab.actionId == "weg_bouwen") {
|
||
Unit *sel = GameManager::instance().getSelectedUnit();
|
||
if (sel && sel->hasRoadRoute()) {
|
||
sel->clearRoadRoute();
|
||
sel->cancelBuilding();
|
||
GameManager::instance().setActionMode(ActionMode::None);
|
||
GameManager::instance().addFloatingText(
|
||
Localization::instance().get("ui.unit.road_route_cancelled", "Road route cancelled"),
|
||
sel->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 120, 100));
|
||
} else if (GameManager::instance().currentActionMode() == ActionMode::BuildRoad) {
|
||
GameManager::instance().setActionMode(ActionMode::None);
|
||
} else {
|
||
GameManager::instance().setActionMode(ActionMode::BuildRoad);
|
||
}
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Compute view matrix for raycasting and 3D screen tests
|
||
QMatrix4x4 vMatrix;
|
||
cameraUpDirection = cameraTransformation * QVector3D(0, 0, 1);
|
||
QVector3D lookat = getCameraLookAt();
|
||
vMatrix.lookAt(cameraPosition, lookat, cameraUpDirection);
|
||
|
||
// 5. Left Click: Check if directly clicked on any unit's Druppel pin
|
||
if (event->button() == Qt::LeftButton) {
|
||
for (const auto &u : GameManager::instance().getUnits()) {
|
||
if (u->isDead()) continue;
|
||
|
||
bool inFront = false;
|
||
QVector3D topPoint = u->worldPos + QVector3D(0, 0, 0.38f);
|
||
QPointF tipPos = UnitRenderer::projectToScreen(topPoint, Projection, vMatrix, width(), height(), inFront);
|
||
if (!inFront) continue;
|
||
|
||
QPointF headCenter(tipPos.x(), tipPos.y() - 25.0f);
|
||
float distSq = (event->x() - headCenter.x()) * (event->x() - headCenter.x()) +
|
||
(event->y() - headCenter.y()) * (event->y() - headCenter.y());
|
||
|
||
if (distSq <= 18.0f * 18.0f) {
|
||
// If in attack mode and clicked enemy: attack!
|
||
Unit *sel = GameManager::instance().getSelectedUnit();
|
||
if (sel && GameManager::instance().currentActionMode() == ActionMode::Attack && u->playerId != sel->playerId) {
|
||
GameManager::instance().commandAttack(u.get());
|
||
} else {
|
||
GameManager::instance().selectUnit(u->id);
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 6. Terrain hex picking via Raycast
|
||
int hitX = -1, hitY = -1;
|
||
bool hitTerrain = screenToHex(event->pos(), vMatrix, hitX, hitY);
|
||
|
||
if (hitTerrain) {
|
||
Unit *selected = GameManager::instance().getSelectedUnit();
|
||
|
||
if (event->button() == Qt::LeftButton) {
|
||
if (selected) {
|
||
if (GameManager::instance().currentActionMode() == ActionMode::Move) {
|
||
GameManager::instance().commandMoveSelected(hitX, hitY, m_point, m_numPoints05);
|
||
GameManager::instance().setActionMode(ActionMode::None);
|
||
update();
|
||
return;
|
||
} else if (GameManager::instance().currentActionMode() == ActionMode::BuildRoad) {
|
||
GameManager::instance().commandBuildRoadToTileSelected(hitX, hitY, m_point, m_numPoints05);
|
||
GameManager::instance().setActionMode(ActionMode::None);
|
||
update();
|
||
return;
|
||
} else if (GameManager::instance().currentActionMode() == ActionMode::Attack) {
|
||
Unit *enemy = GameManager::instance().getUnitAt(hitX, hitY);
|
||
if (enemy && enemy->playerId != selected->playerId) {
|
||
GameManager::instance().commandAttack(enemy);
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Check if there is a unit on hit tile to select
|
||
Unit *unitOnTile = GameManager::instance().getUnitAt(hitX, hitY);
|
||
if (unitOnTile) {
|
||
GameManager::instance().selectUnit(unitOnTile->id);
|
||
} 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);
|
||
} else if (!m_unitCardRect.contains(event->pos())) {
|
||
GameManager::instance().deselectUnit();
|
||
}
|
||
}
|
||
update();
|
||
return;
|
||
} else if (event->button() == Qt::RightButton) {
|
||
if (GameManager::instance().currentActionMode() == ActionMode::BuildRoad) {
|
||
GameManager::instance().setActionMode(ActionMode::None);
|
||
update();
|
||
return;
|
||
}
|
||
// Right click: Attack enemy unit or move selected unit directly to hex!
|
||
if (selected) {
|
||
Unit *enemy = GameManager::instance().getUnitAt(hitX, hitY);
|
||
if (enemy && enemy->playerId != selected->playerId) {
|
||
GameManager::instance().commandAttack(enemy);
|
||
} else {
|
||
GameManager::instance().commandMoveSelected(hitX, hitY, m_point, m_numPoints05);
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
lastPos = event->pos();
|
||
}
|
||
|
||
void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
|
||
{
|
||
m_mouseCurrentPos = event->pos();
|
||
|
||
if (GameManager::instance().isTechTreeOpen()) {
|
||
if (m_draggingTechTree && (event->buttons() & (Qt::LeftButton | Qt::MiddleButton | Qt::RightButton))) {
|
||
int dx = event->x() - lastPos.x();
|
||
int dy = event->y() - lastPos.y();
|
||
if ((event->pos() - m_techTreeDragStartPos).manhattanLength() > 6) {
|
||
m_techTreeDragMoved = true;
|
||
}
|
||
m_techTreeScrollX = std::max(0, m_techTreeScrollX - dx);
|
||
m_techTreeScrollY = std::max(0, m_techTreeScrollY - dy);
|
||
lastPos = event->pos();
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// Disable panning, rotation and minimap drag in City View
|
||
if (GameManager::instance().isInCityView()) {
|
||
if (m_slotPressPending) {
|
||
if ((event->pos() - m_slotPressPos).manhattanLength() > 8) {
|
||
if (m_slotLongPressTimer && m_slotLongPressTimer->isActive()) {
|
||
m_slotLongPressTimer->stop();
|
||
}
|
||
m_slotPressPending = false;
|
||
}
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
|
||
QRect mapRect = getMinimapRect();
|
||
if (m_draggingMinimap && (event->buttons() & Qt::LeftButton)) {
|
||
QVector2D wPos = minimapToWorld(event->pos(), mapRect);
|
||
cameraPosition.setX(wPos.x());
|
||
cameraPosition.setY(wPos.y());
|
||
update();
|
||
return;
|
||
}
|
||
|
||
if (event->buttons() & Qt::RightButton) {
|
||
int dx = event->x() - lastPos.x();
|
||
zRot -= dx * 0.5f;
|
||
if (zRot < 0.0f) zRot += 360.0f;
|
||
if (zRot >= 360.0f) zRot -= 360.0f;
|
||
update();
|
||
}
|
||
|
||
bool onActionItem = false;
|
||
for (const auto &hit : m_actionItemHits) {
|
||
if (hit.rect.contains(event->pos())) {
|
||
onActionItem = true;
|
||
break;
|
||
}
|
||
}
|
||
if (onActionItem || m_techTreeBtnRect.contains(event->pos()) || m_scienceBarHitRect.contains(event->pos())) {
|
||
setCursor(Qt::PointingHandCursor);
|
||
} else {
|
||
unsetCursor();
|
||
}
|
||
|
||
lastPos = event->pos();
|
||
}
|
||
|
||
void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event)
|
||
{
|
||
m_draggingMinimap = false;
|
||
|
||
if (GameManager::instance().isTechTreeOpen()) {
|
||
if (m_draggingTechTree && !m_techTreeDragMoved) {
|
||
// Clicked (without dragging) inside the Tech Tree canvas
|
||
for (const auto &nodeHit : m_techNodeHits) {
|
||
if (nodeHit.rect.contains(event->pos())) {
|
||
if (event->button() == Qt::LeftButton) {
|
||
bool append = (event->modifiers() & Qt::ShiftModifier);
|
||
GameManager::instance().selectTechForPlayer(1, nodeHit.techId, append);
|
||
} else if (event->button() == Qt::RightButton) {
|
||
GameManager::instance().removeTechFromPlayerQueue(1, nodeHit.techId);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
m_draggingTechTree = false;
|
||
m_techTreeDragMoved = false;
|
||
update();
|
||
return;
|
||
}
|
||
|
||
if (m_slotLongPressTimer && m_slotLongPressTimer->isActive()) {
|
||
m_slotLongPressTimer->stop();
|
||
}
|
||
|
||
if (m_slotPressPending && !m_slotLongPressTriggered) {
|
||
// Short click on slot! Toggle worker assignment.
|
||
if (GameManager::instance().isInCityView()) {
|
||
City *city = GameManager::instance().getActiveCityView();
|
||
if (city) {
|
||
int tx = m_pressedSlotTx;
|
||
int ty = m_pressedSlotTy;
|
||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||
float gz = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05);
|
||
if (city->isTileWorked(tx, ty)) {
|
||
city->unassignWorker(tx, ty);
|
||
QString msg = Localization::instance().get("ui.city.worker_freed", "Pop worker freed");
|
||
GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 220, 80));
|
||
} else {
|
||
if (city->getAvailablePop() > 0) {
|
||
QString bestRes = GameManager::instance().getBestTileResource(tx, ty, m_point, m_numPoints05);
|
||
QString res = city->getEffectiveResource(tx, ty, bestRes);
|
||
city->assignWorker(tx, ty, res);
|
||
QString msg = Localization::instance().get("ui.city.worker_assigned", "+1 Pop working");
|
||
GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(80, 255, 120));
|
||
} else {
|
||
QString msg = Localization::instance().get("ui.city.no_free_pop", "No free Pop!");
|
||
GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 80, 80));
|
||
}
|
||
}
|
||
update();
|
||
}
|
||
}
|
||
}
|
||
m_slotPressPending = false;
|
||
m_slotLongPressTriggered = false;
|
||
}
|
||
|
||
void QSceneRenderer::keyPressEvent(QKeyEvent *e)
|
||
{
|
||
if (e->key() == Qt::Key_T && !GameManager::instance().isInCityView()) {
|
||
GameManager::instance().toggleTechTree();
|
||
update();
|
||
return;
|
||
}
|
||
|
||
if (GameManager::instance().isTechTreeOpen()) {
|
||
if (e->key() == Qt::Key_Escape) {
|
||
GameManager::instance().closeTechTree();
|
||
update();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 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) {
|
||
if (m_resourceMenuOpen) {
|
||
m_resourceMenuOpen = false;
|
||
update();
|
||
return;
|
||
}
|
||
m_slotPressPending = false;
|
||
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
|
||
if (e->key() == Qt::Key_A && !(e->modifiers() & Qt::ControlModifier)) {
|
||
cameraPosition.setX(cameraPosition.x() + speed * cos(PI * (zRot + 90) / 180.0f));
|
||
cameraPosition.setY(cameraPosition.y() + speed * sin(PI * (zRot + 90) / 180.0f));
|
||
}
|
||
else if (e->key() == Qt::Key_D) {
|
||
cameraPosition.setX(cameraPosition.x() + speed * cos(PI * (zRot - 90) / 180.0f));
|
||
cameraPosition.setY(cameraPosition.y() + speed * sin(PI * (zRot - 90) / 180.0f));
|
||
}
|
||
else if (e->key() == Qt::Key_W) {
|
||
cameraPosition.setX(cameraPosition.x() + speed * cos(PI * (zRot + 0) / 180.0f));
|
||
cameraPosition.setY(cameraPosition.y() + speed * sin(PI * (zRot + 0) / 180.0f));
|
||
}
|
||
else if (e->key() == Qt::Key_S) {
|
||
cameraPosition.setX(cameraPosition.x() + speed * cos(PI * (zRot + 180) / 180.0f));
|
||
cameraPosition.setY(cameraPosition.y() + speed * sin(PI * (zRot + 180) / 180.0f));
|
||
}
|
||
else if (e->key() == Qt::Key_E) {
|
||
zRot -= 2.0f;
|
||
if (zRot < 0.0f) zRot += 360.0f;
|
||
}
|
||
else if (e->key() == Qt::Key_Q) {
|
||
zRot += 2.0f;
|
||
if (zRot >= 360.0f) zRot -= 360.0f;
|
||
}
|
||
|
||
// Unit action hotkeys
|
||
Unit *sel = GameManager::instance().getSelectedUnit();
|
||
if (e->key() == Qt::Key_M && sel) {
|
||
GameManager::instance().setActionMode(
|
||
GameManager::instance().currentActionMode() == ActionMode::Move ? ActionMode::None : ActionMode::Move
|
||
);
|
||
}
|
||
else if (e->key() == Qt::Key_K && sel && sel->config.canAttack()) {
|
||
GameManager::instance().setActionMode(
|
||
GameManager::instance().currentActionMode() == ActionMode::Attack ? ActionMode::None : ActionMode::Attack
|
||
);
|
||
}
|
||
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_R && sel && sel->config.canBuildRoad()) {
|
||
if (sel->hasRoadRoute()) {
|
||
sel->clearRoadRoute();
|
||
sel->cancelBuilding();
|
||
GameManager::instance().setActionMode(ActionMode::None);
|
||
GameManager::instance().addFloatingText(
|
||
Localization::instance().get("ui.unit.road_route_cancelled", "Road route cancelled"),
|
||
sel->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 120, 100));
|
||
} else {
|
||
GameManager::instance().setActionMode(
|
||
GameManager::instance().currentActionMode() == ActionMode::BuildRoad ? ActionMode::None : ActionMode::BuildRoad
|
||
);
|
||
}
|
||
}
|
||
else if (e->key() == Qt::Key_Escape) {
|
||
if (GameManager::instance().currentActionMode() != ActionMode::None) {
|
||
GameManager::instance().setActionMode(ActionMode::None);
|
||
} else if (sel && sel->hasRoadRoute()) {
|
||
sel->clearRoadRoute();
|
||
sel->cancelBuilding();
|
||
GameManager::instance().addFloatingText(
|
||
Localization::instance().get("ui.unit.road_route_cancelled", "Road route cancelled"),
|
||
sel->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 120, 100));
|
||
} else if (sel && sel->hasQueuedAction()) {
|
||
sel->clearActionQueue();
|
||
GameManager::instance().addFloatingText(
|
||
Localization::instance().get("ui.unit.queue_cleared", "Queued actions cancelled"),
|
||
sel->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80));
|
||
} else {
|
||
GameManager::instance().deselectUnit();
|
||
}
|
||
}
|
||
// Speed hotkeys: 1, 2, 5
|
||
else if (e->key() == Qt::Key_1) {
|
||
GameManager::instance().setSimSpeed(1.0f);
|
||
}
|
||
else if (e->key() == Qt::Key_2) {
|
||
GameManager::instance().setSimSpeed(2.0f);
|
||
}
|
||
else if (e->key() == Qt::Key_3 || e->key() == Qt::Key_5) {
|
||
GameManager::instance().setSimSpeed(5.0f);
|
||
}
|
||
|
||
update();
|
||
}
|
||
|
||
void QSceneRenderer::wheelEvent(QWheelEvent *e)
|
||
{
|
||
if (GameManager::instance().isTechTreeOpen()) {
|
||
if (e->modifiers() & Qt::ShiftModifier) {
|
||
m_techTreeScrollY = std::max(0, m_techTreeScrollY - e->delta() / 2);
|
||
} else {
|
||
m_techTreeScrollX = std::max(0, m_techTreeScrollX - e->delta() / 2);
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// 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) {
|
||
m_minimapZoom = std::min(8.0f, m_minimapZoom * 1.4f);
|
||
} else {
|
||
m_minimapZoom = std::max(1.0f, m_minimapZoom / 1.4f);
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
|
||
m_zoom -= e->delta() / 100.0f;
|
||
if (m_zoom < m_cameraMinZoom) m_zoom = m_cameraMinZoom;
|
||
if (m_zoom > m_cameraMaxZoom) m_zoom = m_cameraMaxZoom;
|
||
update();
|
||
}
|
||
|
||
void QSceneRenderer::initializeGL()
|
||
{
|
||
glEnable(GL_DEPTH_TEST);
|
||
glDepthFunc(GL_LESS);
|
||
|
||
m_unitRenderer.initialize();
|
||
}
|
||
|
||
void QSceneRenderer::resizeGL(int width, int height)
|
||
{
|
||
Projection = QMatrix4x4();
|
||
Projection.perspective(45.0f, (float) width / (float) height, 0.1f, 100.0f);
|
||
}
|
||
|
||
void QSceneRenderer::paintGL()
|
||
{
|
||
glEnable(GL_DEPTH_TEST);
|
||
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);
|
||
|
||
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
|
||
float currentTime = m_elapsedTimer.elapsed() / 1000.0f;
|
||
float deltaTime = m_lastFrameTime > 0.0f ? std::min(0.1f, currentTime - m_lastFrameTime) : 0.016f;
|
||
m_lastFrameTime = currentTime;
|
||
GameManager::instance().update(deltaTime, m_point, m_numPoints05);
|
||
|
||
// Visible terrain tiles bounding box
|
||
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());
|
||
|
||
// 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;
|
||
maxWorldY += margin;
|
||
|
||
float tileStepX = LOD_0_SIZE * 0.75f;
|
||
float tileStepY = LOD_0_SIZE_Y * 2.0f;
|
||
|
||
int minTileX = std::max(0, (int)floor(minWorldX / tileStepX));
|
||
int maxTileX = std::min(MAP_WIDTH - 1, (int)ceil(maxWorldX / tileStepX));
|
||
int minTileY = std::max(0, (int)floor(minWorldY / tileStepY));
|
||
int maxTileY = std::min(MAP_HEIGHT - 1, (int)ceil(maxWorldY / tileStepY));
|
||
|
||
int initMinX = std::max(0, minTileX - 1);
|
||
int initMaxX = std::min(MAP_WIDTH - 1, maxTileX + 1);
|
||
int initMinY = std::max(0, minTileY - 1);
|
||
int initMaxY = std::min(MAP_HEIGHT - 1, maxTileY + 1);
|
||
|
||
for (int y = initMinY; y <= initMaxY; ++y) {
|
||
for (int x = initMinX; x <= initMaxX; ++x) {
|
||
tiles->operator()(x, y).ensureInitialized(this, x, y);
|
||
}
|
||
}
|
||
|
||
for (int y = minTileY; y <= maxTileY; ++y) {
|
||
for (int x = minTileX; x <= maxTileX; ++x) {
|
||
tiles->operator()(x, y).ensureNormalsBuilt();
|
||
}
|
||
}
|
||
|
||
// 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) {
|
||
if (cityView && Pathfinder::hexDistance(cityView->tileX, cityView->tileY, x, y) > cityViewRadius) {
|
||
continue;
|
||
}
|
||
tiles->operator()(x, y).paint(Projection, eyePos, cameraUpDirection, vMatrix, currentTime);
|
||
}
|
||
}
|
||
|
||
// 2. Render 3D Units & Cities (Cylinders & Bastions; roads are rendered directly in the tile shader texture)
|
||
float fSunAngle = 45.0f;
|
||
QVector3D vSunPos(-sin(fSunAngle * PI / 180.0f), -cos(fSunAngle * PI / 180.0f), 0.6f);
|
||
vSunPos.normalize();
|
||
|
||
Unit *selected = GameManager::instance().getSelectedUnit();
|
||
m_unitRenderer.renderUnits(
|
||
GameManager::instance().getUnits(),
|
||
GameManager::instance().getCities(),
|
||
GameManager::instance().players(),
|
||
Projection,
|
||
vMatrix,
|
||
vSunPos,
|
||
selected ? selected->id : -1,
|
||
cityViewRadius,
|
||
cityView ? cityView->tileX : -1,
|
||
cityView ? cityView->tileY : -1
|
||
);
|
||
|
||
// 4. 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 (selected) {
|
||
drawUnitCard(painter, selected);
|
||
}
|
||
drawActionNeededQueue(painter);
|
||
if (GameManager::instance().isTechTreeOpen()) {
|
||
drawTechTreeOverlay(painter);
|
||
}
|
||
} else {
|
||
drawUnitsOverlay(painter, vMatrix);
|
||
drawFloatingTexts(painter, vMatrix);
|
||
drawCityViewOverlay(painter, vMatrix);
|
||
}
|
||
painter.end();
|
||
}
|
||
|
||
void QSceneRenderer::drawActionNeededQueue(QPainter &painter)
|
||
{
|
||
m_actionItemHits.clear();
|
||
m_actionQueueHeaderRect = QRect();
|
||
|
||
const ActionNeededConfig &cfg = GameManager::instance().actionNeededManager().config();
|
||
if (!cfg.enabled) return;
|
||
|
||
std::vector<ActionItem> visibleItems = GameManager::instance().actionNeededManager().getVisibleItems();
|
||
if (visibleItems.empty()) return;
|
||
|
||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||
|
||
QRect minimapCard = getMinimapCardRect();
|
||
int minLeft = minimapCard.right() + 14;
|
||
Unit *selected = GameManager::instance().getSelectedUnit();
|
||
int maxRight = selected ? (m_unitCardRect.left() - 14) : (width() - 16);
|
||
int availableW = maxRight - minLeft;
|
||
if (availableW < 120) return;
|
||
|
||
int itemW = cfg.itemWidth;
|
||
int itemH = cfg.itemHeight;
|
||
int spacing = cfg.itemSpacing;
|
||
int headerW = 82;
|
||
|
||
int remainingForItems = availableW - headerW - spacing;
|
||
int maxItemsThatFit = std::max(1, remainingForItems / (itemW + spacing));
|
||
int numToRender = std::min((int)visibleItems.size(), std::min(cfg.maxVisibleItems, maxItemsThatFit));
|
||
bool hasOverflow = (int)visibleItems.size() > numToRender;
|
||
int overflowW = hasOverflow ? 54 : 0;
|
||
if (hasOverflow && remainingForItems < numToRender * (itemW + spacing) + overflowW) {
|
||
numToRender = std::max(1, numToRender - 1);
|
||
}
|
||
|
||
int totalW = headerW + spacing + numToRender * itemW + (numToRender - 1) * spacing + (hasOverflow ? (spacing + overflowW) : 0);
|
||
int startX = minLeft + (availableW - totalW) / 2;
|
||
if (startX < minLeft) startX = minLeft;
|
||
int bottomY = height() - itemH - cfg.bottomOffset;
|
||
|
||
// 1. Header badge
|
||
QRect headerRect(startX, bottomY, headerW, itemH);
|
||
m_actionQueueHeaderRect = headerRect;
|
||
painter.setBrush(cfg.cardBgColor);
|
||
painter.setPen(QPen(QColor(70, 95, 130, 180), 1.2));
|
||
painter.drawRoundedRect(headerRect, cfg.borderRadius, cfg.borderRadius);
|
||
|
||
float pulse = cfg.pulseAnimation ? (sin(GameManager::instance().actionNeededManager().getElapsedGameTime() * cfg.pulseSpeed) * 0.5f + 0.5f) : 1.0f;
|
||
QRect badgeRect(headerRect.left() + 8, headerRect.top() + (itemH - 20) / 2, 20, 20);
|
||
QColor badgeCol(255, 170, 40);
|
||
badgeCol.setAlpha((int)(170 + 80 * pulse));
|
||
painter.setBrush(badgeCol);
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawEllipse(badgeRect);
|
||
|
||
painter.setFont(QFont("sans-serif", 9, QFont::Bold));
|
||
painter.setPen(Qt::white);
|
||
painter.drawText(badgeRect, Qt::AlignCenter, QString::number(visibleItems.size()));
|
||
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(215, 230, 250));
|
||
QRect headerTextRect(badgeRect.right() + 6, headerRect.top(), headerRect.right() - badgeRect.right() - 8, itemH);
|
||
painter.drawText(headerTextRect, Qt::AlignLeft | Qt::AlignVCenter, Localization::instance().get("ui.action_needed.header_title", "ACTIONS"));
|
||
|
||
// 2. Render cards (oldest first)
|
||
for (int i = 0; i < numToRender; ++i) {
|
||
const ActionItem &item = visibleItems[i];
|
||
int itemX = startX + headerW + spacing + i * (itemW + spacing);
|
||
QRect itemRect(itemX, bottomY, itemW, itemH);
|
||
QRect dismissRect(itemRect.right() - 16, itemRect.top() + 3, 13, 13);
|
||
m_actionItemHits.push_back({itemRect, dismissRect, item.type, item.entityId});
|
||
|
||
bool isHovered = itemRect.contains(m_mouseCurrentPos);
|
||
|
||
QString title;
|
||
QString subtitle;
|
||
QColor accent;
|
||
QImage icon;
|
||
|
||
if (item.type == ActionNeededType::UnitIdle) {
|
||
Unit *u = GameManager::instance().getUnitById(item.entityId);
|
||
title = u ? u->config.name.toUpper() : Localization::instance().get("ui.unit.civilian_unit", "UNIT");
|
||
subtitle = Localization::instance().get("ui.action_needed.unit_idle_desc", "Needs orders");
|
||
accent = cfg.unitIdleColor;
|
||
const Player *p = u ? GameManager::instance().players().getPlayer(u->playerId) : nullptr;
|
||
QColor pCol = p ? p->primaryColor : Qt::blue;
|
||
icon = u ? getTintedIcon(u->config.iconPath, pCol) : QImage();
|
||
} else if (item.type == ActionNeededType::CityNoProduction) {
|
||
City *c = GameManager::instance().getCityById(item.entityId);
|
||
title = c ? c->name : Localization::instance().get("ui.city.center", "CITY");
|
||
subtitle = Localization::instance().get("ui.action_needed.city_no_production_desc", "No production");
|
||
accent = cfg.cityNoProdColor;
|
||
icon = getTintedIcon("images/resources/arbeid.png", accent);
|
||
} else if (item.type == ActionNeededType::CityFreePopulation) {
|
||
City *c = GameManager::instance().getCityById(item.entityId);
|
||
title = c ? c->name : Localization::instance().get("ui.city.center", "CITY");
|
||
subtitle = Localization::instance().get("ui.action_needed.city_free_pop_desc", "+%1 Free Pop").arg(item.auxValue);
|
||
accent = cfg.cityFreePopColor;
|
||
icon = getTintedIcon("images/resources/citizen.png", accent);
|
||
} else if (item.type == ActionNeededType::NoResearch) {
|
||
title = Localization::instance().get("ui.action_needed.no_research_title", "RESEARCH");
|
||
subtitle = Localization::instance().get("ui.action_needed.no_research_desc", "Choose technology");
|
||
accent = cfg.noResearchColor;
|
||
icon = getTintedIcon("images/resources/kennis.png", accent);
|
||
}
|
||
|
||
// Draw card background
|
||
QColor bg = cfg.cardBgColor;
|
||
if (isHovered) {
|
||
bg = bg.lighter(130);
|
||
}
|
||
painter.setBrush(bg);
|
||
painter.setPen(QPen(isHovered ? accent : QColor(70, 95, 130, 160), isHovered ? 1.5 : 1.0));
|
||
painter.drawRoundedRect(itemRect, cfg.borderRadius, cfg.borderRadius);
|
||
|
||
// Accent stripe on the left edge
|
||
QRect stripeRect(itemRect.left() + 2, itemRect.top() + 3, 3, itemH - 6);
|
||
painter.fillRect(stripeRect, accent);
|
||
|
||
// Icon badge
|
||
QRect iconRect(itemRect.left() + 9, itemRect.top() + (itemH - 24) / 2, 24, 24);
|
||
QColor iconBg = accent;
|
||
iconBg.setAlpha(40);
|
||
painter.setBrush(iconBg);
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawEllipse(iconRect);
|
||
if (!icon.isNull()) {
|
||
painter.drawImage(iconRect.adjusted(3, 3, -3, -3), icon);
|
||
}
|
||
|
||
// Title and subtitle text
|
||
int textLeft = iconRect.right() + 7;
|
||
int textW = dismissRect.left() - textLeft - 2;
|
||
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(240, 245, 255));
|
||
painter.drawText(QRect(textLeft, itemRect.top() + 4, textW, 16), Qt::AlignLeft | Qt::AlignVCenter, title);
|
||
|
||
painter.setFont(QFont("sans-serif", 7));
|
||
painter.setPen(accent);
|
||
painter.drawText(QRect(textLeft, itemRect.top() + 20, textW, 15), Qt::AlignLeft | Qt::AlignVCenter, subtitle);
|
||
|
||
// Dismiss [x]
|
||
bool isDismissHovered = dismissRect.contains(m_mouseCurrentPos);
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(isDismissHovered ? QColor(255, 120, 120) : QColor(130, 155, 180, 180));
|
||
painter.drawText(dismissRect, Qt::AlignCenter, "×");
|
||
}
|
||
|
||
// 3. Overflow indicator
|
||
if (hasOverflow) {
|
||
int moreCount = (int)visibleItems.size() - numToRender;
|
||
QRect ofRect(startX + headerW + spacing + numToRender * (itemW + spacing), bottomY, overflowW, itemH);
|
||
painter.setBrush(QColor(20, 26, 38, 220));
|
||
painter.setPen(QPen(QColor(70, 95, 130, 150), 1.0));
|
||
painter.drawRoundedRect(ofRect, cfg.borderRadius, cfg.borderRadius);
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(180, 210, 240));
|
||
QString ofText = Localization::instance().get("ui.action_needed.more_items", "+%1").arg(moreCount);
|
||
painter.drawText(ofRect, Qt::AlignCenter, ofText);
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::handleActionItemClick(ActionNeededType type, int entityId)
|
||
{
|
||
if (type == ActionNeededType::NoResearch) {
|
||
if (GameManager::instance().isInCityView()) {
|
||
GameManager::instance().exitCityView();
|
||
}
|
||
GameManager::instance().openTechTree();
|
||
return;
|
||
}
|
||
|
||
if (type == ActionNeededType::UnitIdle) {
|
||
if (GameManager::instance().isInCityView()) {
|
||
GameManager::instance().exitCityView();
|
||
}
|
||
if (GameManager::instance().isTechTreeOpen()) {
|
||
GameManager::instance().closeTechTree();
|
||
}
|
||
Unit *u = GameManager::instance().getUnitById(entityId);
|
||
if (u) {
|
||
GameManager::instance().selectUnit(entityId);
|
||
cameraPosition.setX(u->worldPos.x());
|
||
cameraPosition.setY(u->worldPos.y());
|
||
}
|
||
} else if (type == ActionNeededType::CityNoProduction || type == ActionNeededType::CityFreePopulation) {
|
||
City *c = GameManager::instance().getCityById(entityId);
|
||
if (c) {
|
||
if (!GameManager::instance().isInCityView()) {
|
||
m_savedCamPos = cameraPosition;
|
||
m_savedZoom = m_zoom;
|
||
m_savedZRot = zRot;
|
||
}
|
||
GameManager::instance().enterCityView(entityId);
|
||
cameraPosition.setX(c->worldPos.x());
|
||
cameraPosition.setY(c->worldPos.y());
|
||
if (type == ActionNeededType::CityNoProduction) {
|
||
m_cityProjectTab = 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
void QSceneRenderer::drawTechTreeOverlay(QPainter &painter)
|
||
{
|
||
m_techNodeHits.clear();
|
||
m_techQueueChipHits.clear();
|
||
m_closeTechTreeBtnRect = QRect();
|
||
m_clearTechQueueBtnRect = QRect();
|
||
|
||
const Player *player = GameManager::instance().players().getPlayer(1);
|
||
if (!player) return;
|
||
|
||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||
|
||
// Modal container below the top tick bar
|
||
QRect modalRect(16, 56, width() - 32, height() - 72);
|
||
painter.setBrush(QColor(12, 17, 26, 248));
|
||
painter.setPen(QPen(QColor(70, 140, 210, 220), 2.0));
|
||
painter.drawRoundedRect(modalRect, 10, 10);
|
||
|
||
int headerH = 84;
|
||
QRect headerRect(modalRect.left() + 14, modalRect.top() + 10, modalRect.width() - 28, headerH);
|
||
|
||
// 1. Title & Science per tick rate
|
||
painter.setFont(QFont("sans-serif", 13, QFont::Bold));
|
||
painter.setPen(QColor(235, 245, 255));
|
||
painter.drawText(QRect(headerRect.left(), headerRect.top(), 240, 26),
|
||
Qt::AlignLeft | Qt::AlignVCenter,
|
||
Localization::instance().get("ui.tech_tree.title", "TECHNOLOGY TREE"));
|
||
|
||
int sciRate = GameManager::instance().calculatePlayerSciencePerTick(1, m_point, m_numPoints05);
|
||
QRect sciBadgeRect(headerRect.left() + 220, headerRect.top() + 2, 130, 24);
|
||
painter.setBrush(QColor(20, 40, 65, 230));
|
||
painter.setPen(QPen(QColor(60, 190, 255), 1.2));
|
||
painter.drawRoundedRect(sciBadgeRect, 5, 5);
|
||
|
||
QImage sciIcon = getTintedIcon("images/resources/kennis.png", QColor(60, 200, 255));
|
||
painter.drawImage(QRect(sciBadgeRect.left() + 6, sciBadgeRect.top() + 4, 16, 16), sciIcon);
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(90, 215, 255));
|
||
painter.drawText(QRect(sciBadgeRect.left() + 26, sciBadgeRect.top(), sciBadgeRect.width() - 30, 24),
|
||
Qt::AlignLeft | Qt::AlignVCenter,
|
||
Localization::instance().get("ui.tech_tree.science_rate", "+%L1/tick").arg(sciRate));
|
||
|
||
// Close button [X]
|
||
m_closeTechTreeBtnRect = QRect(headerRect.right() - 88, headerRect.top() + 2, 88, 26);
|
||
painter.setBrush(QColor(45, 30, 40, 230));
|
||
painter.setPen(QPen(QColor(220, 110, 120), 1.3));
|
||
painter.drawRoundedRect(m_closeTechTreeBtnRect, 5, 5);
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(255, 210, 215));
|
||
painter.drawText(m_closeTechTreeBtnRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.tech_tree.close_btn", "[X] Close"));
|
||
|
||
// 2. Active Research & Queue Strip in Header
|
||
QRect queueStripRect(headerRect.left(), headerRect.top() + 34, headerRect.width(), 44);
|
||
painter.setBrush(QColor(18, 25, 38, 230));
|
||
painter.setPen(QPen(QColor(50, 80, 120, 160), 1.0));
|
||
painter.drawRoundedRect(queueStripRect, 6, 6);
|
||
|
||
if (player->researchQueue.empty()) {
|
||
painter.setFont(QFont("sans-serif", 8, QFont::StyleItalic));
|
||
painter.setPen(QColor(175, 195, 220));
|
||
painter.drawText(queueStripRect.adjusted(14, 0, -14, 0), Qt::AlignLeft | Qt::AlignVCenter,
|
||
Localization::instance().get("ui.tech_tree.no_active_research",
|
||
"No active research - Click any technology node below to start researching"));
|
||
} else {
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(160, 200, 240));
|
||
QString qLabel = Localization::instance().get("ui.tech_tree.queue_label", "QUEUE (%1):")
|
||
.arg(player->researchQueue.size());
|
||
painter.drawText(QRect(queueStripRect.left() + 10, queueStripRect.top(), 85, queueStripRect.height()),
|
||
Qt::AlignLeft | Qt::AlignVCenter, qLabel);
|
||
|
||
int chipX = queueStripRect.left() + 96;
|
||
int maxChipRight = queueStripRect.right() - 100;
|
||
|
||
for (size_t qi = 0; qi < player->researchQueue.size(); ++qi) {
|
||
QString qTechId = player->researchQueue[qi];
|
||
const TechConfig *qCfg = GameManager::instance().techTree().getTech(qTechId);
|
||
if (!qCfg) continue;
|
||
|
||
int prog = player->getTechProgress(qTechId);
|
||
int rem = std::max(0, qCfg->scienceCost - prog);
|
||
int pct = (int)(100.0f * (float)prog / (float)std::max(1, qCfg->scienceCost));
|
||
QString chipText = QString("#%1 %2 (%3% | %L4 left)").arg(qi + 1).arg(qCfg->name).arg(pct).arg(rem);
|
||
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
int chipW = painter.fontMetrics().horizontalAdvance(chipText) + 18;
|
||
if (chipX + chipW > maxChipRight) break;
|
||
|
||
QRect chipRect(chipX, queueStripRect.top() + 8, chipW, 28);
|
||
bool isFirst = (qi == 0);
|
||
painter.setBrush(isFirst ? QColor(28, 65, 105, 245) : QColor(24, 38, 60, 230));
|
||
painter.setPen(QPen(isFirst ? QColor(80, 215, 255) : QColor(70, 120, 180), isFirst ? 1.5 : 1.0));
|
||
painter.drawRoundedRect(chipRect, 4, 4);
|
||
|
||
painter.setPen(isFirst ? QColor(230, 250, 255) : QColor(190, 215, 245));
|
||
painter.drawText(chipRect, Qt::AlignCenter, chipText);
|
||
|
||
m_techQueueChipHits.push_back({chipRect, qTechId});
|
||
chipX += chipW + 8;
|
||
}
|
||
|
||
// Clear Queue Button
|
||
m_clearTechQueueBtnRect = QRect(queueStripRect.right() - 90, queueStripRect.top() + 8, 82, 28);
|
||
painter.setBrush(QColor(55, 28, 34, 225));
|
||
painter.setPen(QPen(QColor(210, 90, 95), 1.0));
|
||
painter.drawRoundedRect(m_clearTechQueueBtnRect, 4, 4);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(255, 165, 170));
|
||
painter.drawText(m_clearTechQueueBtnRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.tech_tree.clear_queue_btn", "Clear Queue"));
|
||
}
|
||
|
||
// 3. Tech Tree Canvas (Scrollable Grid of Technology Cards + Dependency Connectors)
|
||
int footerH = 26;
|
||
m_techTreeCanvasRect = QRect(modalRect.left() + 14,
|
||
headerRect.bottom() + 8,
|
||
modalRect.width() - 28,
|
||
modalRect.height() - headerH - footerH - 26);
|
||
|
||
painter.setBrush(QColor(9, 13, 20, 245));
|
||
painter.setPen(QPen(QColor(45, 70, 105, 180), 1.2));
|
||
painter.drawRoundedRect(m_techTreeCanvasRect, 8, 8);
|
||
|
||
// Layout constants for tech cards
|
||
const int nodeW = 236;
|
||
const int nodeH = 104;
|
||
const int colSpacing = 66;
|
||
const int rowSpacing = 22;
|
||
const int padX = 24;
|
||
const int padY = 20;
|
||
|
||
int maxCol = GameManager::instance().techTree().getMaxColumn();
|
||
int maxRow = GameManager::instance().techTree().getMaxRow();
|
||
int contentW = padX * 2 + (maxCol + 1) * nodeW + maxCol * colSpacing;
|
||
int contentH = padY * 2 + (maxRow + 1) * nodeH + maxRow * rowSpacing;
|
||
|
||
int maxScrollX = std::max(0, contentW - m_techTreeCanvasRect.width());
|
||
int maxScrollY = std::max(0, contentH - m_techTreeCanvasRect.height());
|
||
m_techTreeScrollX = std::max(0, std::min(m_techTreeScrollX, maxScrollX));
|
||
m_techTreeScrollY = std::max(0, std::min(m_techTreeScrollY, maxScrollY));
|
||
|
||
const auto &allTechs = GameManager::instance().techTree().getAllTechs();
|
||
const auto &orderedIds = GameManager::instance().techTree().getOrderedTechIds();
|
||
|
||
// Compute screen rectangles for all tech nodes
|
||
QMap<QString, QRect> nodeRects;
|
||
for (const QString &tId : orderedIds) {
|
||
const TechConfig &cfg = allTechs[tId];
|
||
int nx = m_techTreeCanvasRect.left() + padX + cfg.column * (nodeW + colSpacing) - m_techTreeScrollX;
|
||
int ny = m_techTreeCanvasRect.top() + padY + cfg.row * (nodeH + rowSpacing) - m_techTreeScrollY;
|
||
nodeRects[tId] = QRect(nx, ny, nodeW, nodeH);
|
||
}
|
||
|
||
painter.save();
|
||
painter.setClipRect(m_techTreeCanvasRect.adjusted(2, 2, -2, -2));
|
||
|
||
// Subtle column tier background stripes
|
||
for (int c = 0; c <= maxCol; ++c) {
|
||
int colLeft = m_techTreeCanvasRect.left() + padX + c * (nodeW + colSpacing) - m_techTreeScrollX - 10;
|
||
QRect colBand(colLeft, m_techTreeCanvasRect.top() + 4, nodeW + 20, m_techTreeCanvasRect.height() - 8);
|
||
if (c % 2 == 1) {
|
||
painter.fillRect(colBand, QColor(18, 26, 40, 65));
|
||
}
|
||
}
|
||
|
||
// 3a. Draw Prerequisite Connector Curves (from prereq right edge to dependent left edge)
|
||
for (const QString &tId : orderedIds) {
|
||
const TechConfig &cfg = allTechs[tId];
|
||
QRect targetR = nodeRects[tId];
|
||
bool targetResearched = player->hasResearched(tId);
|
||
bool targetQueued = player->isTechQueued(tId);
|
||
|
||
for (const QString &preId : cfg.prerequisites) {
|
||
if (!nodeRects.contains(preId)) continue;
|
||
QRect srcR = nodeRects[preId];
|
||
|
||
QPointF pStart(srcR.right(), srcR.center().y());
|
||
QPointF pEnd(targetR.left(), targetR.center().y());
|
||
float ctrlOffset = std::max(28.0f, (float)(pEnd.x() - pStart.x()) * 0.48f);
|
||
|
||
QPainterPath curve;
|
||
curve.moveTo(pStart);
|
||
curve.cubicTo(QPointF(pStart.x() + ctrlOffset, pStart.y()),
|
||
QPointF(pEnd.x() - ctrlOffset, pEnd.y()),
|
||
pEnd);
|
||
|
||
bool preResearched = player->hasResearched(preId);
|
||
bool preQueued = player->isTechQueued(preId);
|
||
|
||
QColor lineCol;
|
||
float lineW = 2.0f;
|
||
if (preResearched && targetResearched) {
|
||
lineCol = QColor(70, 215, 115, 220);
|
||
lineW = 2.5f;
|
||
} else if (preResearched) {
|
||
lineCol = QColor(80, 195, 255, 210);
|
||
lineW = 2.2f;
|
||
} else if (preQueued && targetQueued) {
|
||
lineCol = QColor(255, 210, 70, 210);
|
||
lineW = 2.2f;
|
||
} else {
|
||
lineCol = QColor(70, 95, 130, 150);
|
||
lineW = 1.6f;
|
||
}
|
||
|
||
painter.setPen(QPen(lineCol, lineW));
|
||
painter.setBrush(Qt::NoBrush);
|
||
painter.drawPath(curve);
|
||
|
||
// Arrowhead at pEnd
|
||
QPolygonF arrowHead;
|
||
arrowHead << pEnd
|
||
<< QPointF(pEnd.x() - 7.0f, pEnd.y() - 4.5f)
|
||
<< QPointF(pEnd.x() - 7.0f, pEnd.y() + 4.5f);
|
||
painter.setBrush(lineCol);
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawPolygon(arrowHead);
|
||
}
|
||
}
|
||
|
||
// 3b. Draw Technology Cards
|
||
int effectiveSciRate = std::max(1, sciRate);
|
||
for (const QString &tId : orderedIds) {
|
||
const TechConfig &cfg = allTechs[tId];
|
||
QRect nRect = nodeRects[tId];
|
||
if (!m_techTreeCanvasRect.intersects(nRect)) continue;
|
||
|
||
m_techNodeHits.push_back({nRect, tId});
|
||
|
||
bool isResearched = player->hasResearched(tId);
|
||
int qPos = player->getTechQueuePosition(tId);
|
||
bool isActive = (qPos == 1);
|
||
bool isQueued = (qPos > 1);
|
||
bool isAvailable = !isResearched && player->arePrerequisitesMet(cfg.prerequisites);
|
||
bool isHovered = nRect.contains(m_mouseCurrentPos);
|
||
|
||
int curProg = isResearched ? cfg.scienceCost : player->getTechProgress(tId);
|
||
int remSci = std::max(0, cfg.scienceCost - curProg);
|
||
float progFrac = std::max(0.0f, std::min(1.0f, (float)curProg / (float)std::max(1, cfg.scienceCost)));
|
||
int pct = (int)(progFrac * 100.0f);
|
||
int estTicks = (int)std::ceil((float)remSci / (float)effectiveSciRate);
|
||
|
||
// Card background & border based on state
|
||
QColor bgCol, borderCol, badgeBgCol, badgeTextCol;
|
||
QString statusText;
|
||
|
||
if (isResearched) {
|
||
bgCol = QColor(20, 42, 30, 240);
|
||
borderCol = QColor(75, 220, 120);
|
||
badgeBgCol = QColor(30, 75, 45);
|
||
badgeTextCol = QColor(120, 255, 155);
|
||
statusText = Localization::instance().get("ui.tech_tree.status_researched", "RESEARCHED");
|
||
} else if (isActive) {
|
||
bgCol = QColor(22, 46, 74, 245);
|
||
borderCol = QColor(80, 225, 255);
|
||
badgeBgCol = QColor(35, 95, 140);
|
||
badgeTextCol = QColor(160, 245, 255);
|
||
statusText = Localization::instance().get("ui.tech_tree.status_researching", "ACTIVE #1");
|
||
} else if (isQueued) {
|
||
bgCol = QColor(24, 38, 62, 240);
|
||
borderCol = QColor(255, 205, 65);
|
||
badgeBgCol = QColor(75, 60, 20);
|
||
badgeTextCol = QColor(255, 225, 100);
|
||
statusText = Localization::instance().get("ui.tech_tree.status_queued", "QUEUED #%1").arg(qPos);
|
||
} else if (isAvailable) {
|
||
bgCol = QColor(22, 34, 52, 235);
|
||
borderCol = QColor(90, 165, 235);
|
||
badgeBgCol = QColor(30, 55, 85);
|
||
badgeTextCol = QColor(160, 215, 255);
|
||
statusText = Localization::instance().get("ui.tech_tree.status_available", "AVAILABLE");
|
||
} else {
|
||
bgCol = QColor(18, 23, 34, 225);
|
||
borderCol = QColor(65, 80, 105);
|
||
badgeBgCol = QColor(32, 38, 52);
|
||
badgeTextCol = QColor(150, 165, 190);
|
||
statusText = Localization::instance().get("ui.tech_tree.status_locked", "LOCKED");
|
||
}
|
||
|
||
if (isHovered && !isResearched) {
|
||
bgCol = bgCol.lighter(122);
|
||
borderCol = borderCol.lighter(125);
|
||
}
|
||
|
||
painter.setBrush(bgCol);
|
||
painter.setPen(QPen(borderCol, (isActive || isQueued || isHovered) ? 2.0 : 1.3));
|
||
painter.drawRoundedRect(nRect, 7, 7);
|
||
|
||
// Icon
|
||
QImage tIcon = getTintedIcon(cfg.iconPath, isResearched ? QColor(100, 245, 140) : QColor(90, 205, 255));
|
||
painter.drawImage(QRect(nRect.left() + 8, nRect.top() + 8, 22, 22), tIcon);
|
||
|
||
// Tech Name
|
||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||
painter.setPen(QColor(240, 248, 255));
|
||
painter.drawText(QRect(nRect.left() + 34, nRect.top() + 6, nRect.width() - 112, 16),
|
||
Qt::AlignLeft | Qt::AlignVCenter, cfg.name);
|
||
|
||
// Status Pill (Top-Right)
|
||
QRect statusRect(nRect.right() - 74, nRect.top() + 6, 68, 16);
|
||
painter.setBrush(badgeBgCol);
|
||
painter.setPen(QPen(borderCol, 0.8));
|
||
painter.drawRoundedRect(statusRect, 3, 3);
|
||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||
painter.setPen(badgeTextCol);
|
||
painter.drawText(statusRect, Qt::AlignCenter, statusText);
|
||
|
||
// Science Cost / Partial Progress Bar
|
||
QRect progBarBg(nRect.left() + 34, nRect.top() + 24, nRect.width() - 42, 13);
|
||
painter.setBrush(QColor(12, 18, 26));
|
||
painter.setPen(QPen(QColor(45, 65, 95), 0.8));
|
||
painter.drawRoundedRect(progBarBg, 3, 3);
|
||
|
||
if (curProg > 0) {
|
||
QRect progBarFg(progBarBg.left(), progBarBg.top(), (int)(progBarBg.width() * progFrac), progBarBg.height());
|
||
painter.setBrush(isResearched ? QColor(50, 175, 90) : QColor(45, 155, 225));
|
||
painter.setPen(Qt::NoPen);
|
||
painter.drawRoundedRect(progBarFg, 3, 3);
|
||
}
|
||
|
||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||
painter.setPen(Qt::white);
|
||
QString costLabel;
|
||
if (isResearched) {
|
||
costLabel = QString("%L1 / %L2 (100%)").arg(cfg.scienceCost).arg(cfg.scienceCost);
|
||
} else if (curProg > 0) {
|
||
costLabel = QString("%L1 / %L2 (%3%) - ~%4t")
|
||
.arg(curProg).arg(cfg.scienceCost).arg(pct).arg(estTicks);
|
||
} else {
|
||
QString tickWord = (estTicks == 1)
|
||
? Localization::instance().get("ui.tech_tree.tick_singular", "tick")
|
||
: Localization::instance().get("ui.tech_tree.tick_plural", "ticks");
|
||
costLabel = Localization::instance().get("ui.tech_tree.cost_format", "%L1 Science (~%2 %3)")
|
||
.arg(cfg.scienceCost).arg(estTicks).arg(tickWord);
|
||
}
|
||
painter.drawText(progBarBg, Qt::AlignCenter, costLabel);
|
||
|
||
// Unlocks & Obsoletes lines
|
||
int infoY = nRect.top() + 42;
|
||
QStringList unlocks = GameManager::instance().getTechUnlocks(tId);
|
||
QStringList obsoletes = GameManager::instance().getTechObsoletes(tId);
|
||
|
||
if (!unlocks.isEmpty()) {
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(110, 240, 150));
|
||
QString uStr = Localization::instance().get("ui.tech_tree.unlocks_label", "Unlocks: %1")
|
||
.arg(unlocks.join(", "));
|
||
painter.drawText(QRect(nRect.left() + 8, infoY, nRect.width() - 16, 14),
|
||
Qt::AlignLeft | Qt::AlignVCenter, uStr);
|
||
infoY += 14;
|
||
}
|
||
|
||
if (!obsoletes.isEmpty()) {
|
||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||
painter.setPen(QColor(255, 155, 95));
|
||
QString oStr = Localization::instance().get("ui.tech_tree.obsoletes_label", "Obsoletes: %1")
|
||
.arg(obsoletes.join(", "));
|
||
painter.drawText(QRect(nRect.left() + 8, infoY, nRect.width() - 16, 14),
|
||
Qt::AlignLeft | Qt::AlignVCenter, oStr);
|
||
infoY += 14;
|
||
}
|
||
|
||
// Description text in remaining bottom space
|
||
if (infoY + 12 <= nRect.bottom() - 2) {
|
||
painter.setFont(QFont("sans-serif", 6));
|
||
painter.setPen(QColor(165, 185, 210));
|
||
painter.drawText(QRect(nRect.left() + 8, infoY, nRect.width() - 16, nRect.bottom() - infoY - 3),
|
||
Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, cfg.description);
|
||
}
|
||
}
|
||
|
||
painter.restore();
|
||
|
||
// 4. Footer Help Text
|
||
QRect footerRect(modalRect.left() + 16, modalRect.bottom() - footerH - 4, modalRect.width() - 32, footerH);
|
||
painter.setFont(QFont("sans-serif", 7, QFont::StyleItalic));
|
||
painter.setPen(QColor(150, 180, 215));
|
||
painter.drawText(footerRect, Qt::AlignCenter,
|
||
Localization::instance().get("ui.tech_tree.help_text",
|
||
"Left-click a tech to research (prerequisites auto-queued by least remaining work) | Shift+Click to append | Right-click to remove | Drag/Scroll to pan"));
|
||
}
|
||
|