1560 lines
64 KiB
C++
1560 lines
64 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();
|
|
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;
|
|
|
|
QRect bannerRect((int)screenP.x() - 55, (int)screenP.y() - 14, 110, 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());
|
|
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
|
|
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::drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix)
|
|
{
|
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
for (const auto &city : GameManager::instance().getCities()) {
|
|
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::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
|
|
{
|
|
m_citySlotHits.clear();
|
|
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 - 1; dx <= maxRadius + 1; ++dx) {
|
|
for (int dy = -maxRadius - 1; dy <= maxRadius + 1; ++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 res = isWorked ? city->getWorkedResource(hx, hy) : yCfg->primaryResource;
|
|
int amount = yCfg->getYield(res);
|
|
|
|
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);
|
|
|
|
// 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);
|
|
|
|
// 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 {
|
|
// Outside borders: draw subtle locked badge
|
|
QRect lockRect((int)sCenter.x() - 12, (int)sCenter.y() - 7, 24, 14);
|
|
painter.setBrush(QColor(20, 24, 32, 180));
|
|
painter.setPen(QPen(QColor(100, 120, 140, 100), 1.0));
|
|
painter.drawRoundedRect(lockRect, 3, 3);
|
|
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
|
painter.setPen(QColor(140, 155, 175));
|
|
painter.drawText(lockRect, Qt::AlignCenter, "SLOT");
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. City Management Top Card
|
|
int cardW = 500, cardH = 145;
|
|
QRect cardRect((width() - cardW) / 2, 16, cardW, cardH);
|
|
|
|
painter.setBrush(QColor(16, 22, 32, 242));
|
|
painter.setPen(QPen(primaryCol, 1.8));
|
|
painter.drawRoundedRect(cardRect, 10, 10);
|
|
|
|
// City Title & Close Button
|
|
painter.setFont(QFont("sans-serif", 13, QFont::Bold));
|
|
painter.setPen(QColor(245, 250, 255));
|
|
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 10, 260, 24), Qt::AlignLeft | Qt::AlignVCenter, city->name.toUpper() + " - STADSBEHEER");
|
|
|
|
// Close button [X]
|
|
m_closeCityViewBtnRect = QRect(cardRect.right() - 88, cardRect.top() + 10, 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, "[X] Sluiten");
|
|
|
|
// Population Stats Row
|
|
int totalPop = city->getTotalPop();
|
|
int assigned = city->getAssignedPop();
|
|
int avail = city->getAvailablePop();
|
|
|
|
painter.setFont(QFont("sans-serif", 9));
|
|
painter.setPen(QColor(180, 205, 235));
|
|
QString popInfo = QString("Inwoners: %L1 | Totale Pop: %2 | Aan het werk: %3 | Beschikbaar: %4")
|
|
.arg(city->inhabitants).arg(totalPop).arg(assigned).arg(avail);
|
|
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 38, cardRect.width() - 32, 18), Qt::AlignLeft | Qt::AlignVCenter, popInfo);
|
|
|
|
// Separator line
|
|
painter.setPen(QPen(QColor(60, 85, 120, 150), 1.0));
|
|
painter.drawLine(cardRect.left() + 16, cardRect.top() + 60, cardRect.right() - 16, cardRect.top() + 60);
|
|
|
|
// Total Yields Summary Row
|
|
CityYieldSummary summary = GameManager::instance().calculateCityYields(*city, m_point, m_numPoints05);
|
|
|
|
struct YieldItem { QString name; int val; QString icon; QColor col; };
|
|
std::vector<YieldItem> yields = {
|
|
{"Voedsel", summary.food, "images/resources/voedsel.png", QColor(100, 230, 110)},
|
|
{"Arbeid", summary.labor, "images/resources/arbeid.png", QColor(240, 140, 50)},
|
|
{"Kennis", summary.science, "images/resources/kennis.png", QColor(60, 190, 255)},
|
|
{"Cultuur", summary.culture, "images/resources/cultuur.png", QColor(200, 110, 240)},
|
|
{"Geloof", summary.faith, "images/resources/geloof.png", QColor(255, 230, 80)},
|
|
{"Goud", 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() + 68, colW - 6, 44);
|
|
|
|
painter.setBrush(QColor(12, 16, 24, 180));
|
|
painter.setPen(QPen(QColor(40, 60, 85, 120), 1.0));
|
|
painter.drawRoundedRect(yRect, 4, 4);
|
|
|
|
QImage icon = getTintedIcon(yields[i].icon, yields[i].col);
|
|
painter.drawImage(QRect(yRect.left() + 6, yRect.top() + 6, 16, 16), icon);
|
|
|
|
painter.setFont(QFont("sans-serif", 7));
|
|
painter.setPen(QColor(160, 180, 205));
|
|
painter.drawText(QRect(yRect.left() + 26, yRect.top() + 6, yRect.width() - 28, 16), Qt::AlignLeft | Qt::AlignVCenter, yields[i].name);
|
|
|
|
painter.setFont(QFont("sans-serif", 9, QFont::Bold));
|
|
painter.setPen(yields[i].col);
|
|
painter.drawText(QRect(yRect.left() + 6, yRect.top() + 24, yRect.width() - 12, 16), Qt::AlignLeft | Qt::AlignVCenter, QString("+%1").arg(yields[i].val));
|
|
}
|
|
|
|
// Help instruction at bottom
|
|
painter.setFont(QFont("sans-serif", 7, QFont::StyleItalic));
|
|
painter.setPen(QColor(140, 170, 200));
|
|
painter.drawText(QRect(cardRect.left() + 16, cardRect.bottom() - 20, cardRect.width() - 32, 16), Qt::AlignCenter,
|
|
"Klik op een tegel binnen de grenzen om 1 Pop toe te wijzen of vrij te maken.");
|
|
}
|
|
|
|
void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
// 0. If in City View: handle close button and slot clicks
|
|
if (GameManager::instance().isInCityView()) {
|
|
if (m_closeCityViewBtnRect.contains(event->pos())) {
|
|
GameManager::instance().exitCityView();
|
|
update();
|
|
return;
|
|
}
|
|
|
|
for (const auto &slot : m_citySlotHits) {
|
|
if (slot.rect.contains(event->pos())) {
|
|
City *city = GameManager::instance().getActiveCityView();
|
|
if (city) {
|
|
QVector2D w = Pathfinder::hexToWorld(slot.tx, slot.ty);
|
|
float gz = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05);
|
|
if (city->isTileWorked(slot.tx, slot.ty)) {
|
|
city->unassignWorker(slot.tx, slot.ty);
|
|
GameManager::instance().addFloatingText("Pop vrijgemaakt", QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 220, 80));
|
|
} else {
|
|
if (city->getAvailablePop() > 0) {
|
|
city->assignWorker(slot.tx, slot.ty, slot.defaultRes);
|
|
GameManager::instance().addFloatingText("+1 Pop aan het werk", QVector3D(w.x(), w.y(), gz + 0.8f), QColor(80, 255, 120));
|
|
} else {
|
|
GameManager::instance().addFloatingText("Geen vrije Pop!", QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 80, 80));
|
|
}
|
|
}
|
|
update();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if clicked on a City Banner to open City View
|
|
for (const auto &cb : m_cityBannerHits) {
|
|
if (cb.rect.contains(event->pos())) {
|
|
GameManager::instance().enterCityView(cb.cityId);
|
|
City *c = GameManager::instance().getCityById(cb.cityId);
|
|
if (c) {
|
|
cameraPosition.setX(c->worldPos.x());
|
|
cameraPosition.setY(c->worldPos.y() - 2.8f);
|
|
}
|
|
update();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
City *cityOnTile = GameManager::instance().getCityAt(hitX, hitY);
|
|
if (cityOnTile) {
|
|
GameManager::instance().enterCityView(cityOnTile->id);
|
|
cameraPosition.setX(cityOnTile->worldPos.x());
|
|
cameraPosition.setY(cityOnTile->worldPos.y() - 2.8f);
|
|
} else if (!m_unitCardRect.contains(event->pos())) {
|
|
GameManager::instance().deselectUnit();
|
|
}
|
|
}
|
|
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().isInCityView()) {
|
|
GameManager::instance().exitCityView();
|
|
} else 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);
|
|
drawCityBorders(painter, vMatrix);
|
|
drawPathAndRange(painter, selected, vMatrix);
|
|
drawMinimap(painter);
|
|
drawTickBar(painter);
|
|
drawUnitsOverlay(painter, vMatrix);
|
|
drawFloatingTexts(painter, vMatrix);
|
|
|
|
if (GameManager::instance().isInCityView()) {
|
|
drawCityViewOverlay(painter, vMatrix);
|
|
} else if (selected) {
|
|
drawUnitCard(painter, selected);
|
|
}
|
|
painter.end();
|
|
}
|