Files
Ascent-of-Man/qscenerenderer.cpp
T
2026-11-08 15:42:54 +01:00

1257 lines
50 KiB
C++

#include "qscenerenderer.h"
#include <algorithm>
#include <cmath>
#include <QPainter>
#include <QPainterPath>
#include <QToolTip>
#include <QDebug>
#include "pathfinder.h"
GLuint vertexbuffer;
GLuint VertexArrayID;
QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
{
setFocusPolicy(Qt::FocusPolicy::StrongFocus);
setMouseTracking(true);
installEventFilter(this);
QImage img = QImage("/home/bart/project/tiletest/images/heightmap.jpg").convertToFormat(QImage::Format_RGB32);
QImage typeImg = QImage("/home/bart/project/tiletest/images/typemap.png").convertToFormat(QImage::Format_RGB32);
QImage riverImg = QImage("/home/bart/project/tiletest/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;
}
}
}
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/project/tiletest/images/minimap.png");
if (m_image_minimap.isNull()) {
m_image_minimap = QImage("/home/bart/project/tiletest/images/rivermap_preview.png");
}
// Initialize GameManager and spawn demo units
GameManager::instance().init("/home/bart/project/tiletest", 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);
}
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 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 viewDist = std::max(12.0f, m_zoom * 2.2f);
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")
.arg(cameraPosition.x(), 0, 'f', 1)
.arg(cameraPosition.y(), 0, 'f', 1)
.arg(m_zoom, 0, 'f', 1);
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/project/tiletest/" + 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 = 340, barH = 34;
QRect barRect(16, 16, barW, barH);
// Dark glass card
painter.setBrush(QColor(18, 24, 34, 230));
painter.setPen(QPen(QColor(70, 100, 140, 180), 1.2));
painter.drawRoundedRect(barRect, 6, 6);
// 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, 140, 6);
painter.setBrush(QColor(30, 40, 55));
painter.setPen(Qt::NoPen);
painter.drawRoundedRect(progBg, 3, 3);
QRect progFg(barRect.left() + 10, barRect.top() + 20, (int)(140 * progress), 6);
painter.setBrush(QColor(60, 180, 255));
painter.drawRoundedRect(progFg, 3, 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();
painter.setRenderHint(QPainter::Antialiasing, true);
Unit *selectedUnit = GameManager::instance().getSelectedUnit();
// 1. Draw City Banners
for (const auto &c : GameManager::instance().getCities()) {
bool inFront = false;
QPointF screenP = UnitRenderer::projectToScreen(c.worldPos + QVector3D(0, 0, 0.65f), Projection, vMatrix, width(), height(), inFront);
if (!inFront) continue;
const Player *pl = GameManager::instance().players().getPlayer(c.playerId);
QColor col = pl ? pl->primaryColor : Qt::blue;
QRect bannerRect((int)screenP.x() - 40, (int)screenP.y() - 14, 80, 20);
painter.setBrush(QColor(20, 24, 34, 230));
painter.setPen(QPen(col, 1.8));
painter.drawRoundedRect(bannerRect, 4, 4);
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
painter.setPen(Qt::white);
painter.drawText(bannerRect, Qt::AlignCenter, c.name);
}
// 2. Draw Units: Teardrop (Druppel in secondary color) + Icon (in primary color)
const float R = 14.0f; // Head circle radius
for (const auto &unit : GameManager::instance().getUnits()) {
if (unit->isDead()) 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);
// 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 = QString("Aanvallen (A) [Bereik: %1]").arg(unit->config.attackRange);
} else if (act == "steden_bouwen" || act == "build_city") {
isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity);
iconFile = "images/actions/build_city.png";
tip = "Steden bouwen (B)";
}
// 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. 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 = QString("Snelheid: %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 = QString("Kosten: %1 pnt").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 = QString("Aanval: %1 | Bereik: %2 hex").arg(unit->config.attackDamage).arg(unit->config.attackRange);
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, combatInfo);
} else {
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, "Civiele eenheid");
}
// 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 = "Verplaats";
QString tip = "Verplaatsen (M)";
if (act == "verplaatsen") {
isActive = (GameManager::instance().currentActionMode() == ActionMode::Move);
iconFile = "images/actions/move.png";
label = "Beweeg [M]";
tip = "Verplaatsen (M)";
} else if (act == "aanvallen") {
isActive = (GameManager::instance().currentActionMode() == ActionMode::Attack);
iconFile = "images/actions/attack.png";
label = "Aanval [A]";
tip = QString("Aanvallen (A) [Bereik: %1]").arg(unit->config.attackRange);
} else if (act == "steden_bouwen" || act == "build_city") {
isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity);
iconFile = "images/actions/build_city.png";
label = "Sticht [B]";
tip = "Steden bouwen (B)";
}
// 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::mousePressEvent(QMouseEvent *event)
{
// 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);
}
update();
return;
}
}
// Compute view matrix for raycasting and 3D screen tests
QMatrix4x4 vMatrix;
cameraUpDirection = cameraTransformation * QVector3D(0, 0, 1);
QVector3D lookat = QVector3D(
cameraPosition.x() + m_zoom * m_zoom / 5.0f * cos(PI * zRot / 180.0f),
cameraPosition.y() + m_zoom * m_zoom / 5.0f * sin(PI * zRot / 180.0f),
0
);
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::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 if (!m_unitCardRect.contains(event->pos())) {
GameManager::instance().deselectUnit();
}
update();
return;
} else if (event->button() == Qt::RightButton) {
// Right click: Move selected unit directly to hex!
if (selected) {
GameManager::instance().commandMoveSelected(hitX, hitY, m_point, m_numPoints05);
update();
return;
}
}
}
lastPos = event->pos();
}
void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
{
m_mouseCurrentPos = event->pos();
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();
}
lastPos = event->pos();
}
void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event)
{
Q_UNUSED(event);
m_draggingMinimap = false;
}
void QSceneRenderer::keyPressEvent(QKeyEvent *e)
{
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_Escape) {
if (GameManager::instance().currentActionMode() != ActionMode::None) {
GameManager::instance().setActionMode(ActionMode::None);
} 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)
{
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 < 2.0f) m_zoom = 2.0f;
if (m_zoom > 16.0f) m_zoom = 16.0f;
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;
cameraTrans.rotate(zRot, 0, 0, 1);
cameraUpDirection = cameraTrans * QVector3D(0, 0, 1);
cameraPosition.setZ(m_zoom);
QVector3D lookat = QVector3D(
cameraPosition.x() + m_zoom * m_zoom / 5.0f * cos(PI * zRot / 180.0f),
cameraPosition.y() + m_zoom * m_zoom / 5.0f * sin(PI * zRot / 180.0f),
0.0f
);
vMatrix.lookAt(cameraPosition, lookat, cameraUpDirection);
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(cameraPosition.x(), lookat.x());
float maxWorldX = std::max(cameraPosition.x(), lookat.x());
float minWorldY = std::min(cameraPosition.y(), lookat.y());
float maxWorldY = std::max(cameraPosition.y(), lookat.y());
float margin = std::max(m_zoom * 3.0f + 15.0f, 35.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
for (int y = minTileY; y <= maxTileY; ++y) {
for (int x = minTileX; x <= maxTileX; ++x) {
tiles->operator()(x, y).paint(Projection, cameraPosition, cameraUpDirection, vMatrix, currentTime);
}
}
// 2. Render 3D Units (Cylinders)
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
);
// 3. Render 2D UI & HUD Overlays via QPainter
QPainter painter(this);
drawPathAndRange(painter, selected, vMatrix);
drawMinimap(painter);
drawTickBar(painter);
drawUnitsOverlay(painter, vMatrix);
drawFloatingTexts(painter, vMatrix);
if (selected) {
drawUnitCard(painter, selected);
}
painter.end();
}