#include "qscenerenderer.h" #include #include #include #include #include #include #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(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(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 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(); }