Implement builder road construction with hex-edge auto-connection, winding rendering, and speed bonus

This commit is contained in:
bart
2026-11-08 21:52:34 +01:00
parent 4b237872eb
commit dfefb2d7fb
15 changed files with 620 additions and 16 deletions
+177 -7
View File
@@ -347,6 +347,31 @@ void QSceneRenderer::drawMinimap(QPainter &painter)
painter.setPen(QPen(QColor(120, 150, 180, 160), 1.0));
painter.drawRect(mapRect);
// Draw roads on minimap
for (const auto &pair : GameManager::instance().getTileRoads()) {
const TileRoad &road = pair.second;
QVector2D center = Pathfinder::hexToWorld(road.tileX, road.tileY);
QPointF mapP1 = worldToMinimap(center.x(), center.y(), mapRect);
if (!mapRect.contains(mapP1.toPoint())) continue;
const RoadTypeConfig *rCfg = GameManager::instance().roadConfigs().getRoadType(road.roadTypeId);
QColor roadCol = rCfg ? rCfg->getColor() : QColor(194, 166, 117);
painter.setPen(QPen(roadCol, 1.2f));
if (road.connectionMask == 0) {
painter.setBrush(roadCol);
painter.drawEllipse(mapP1, 1.2, 1.2);
} else {
for (int e = 0; e < 6; ++e) {
if (road.hasConnection(e)) {
QVector2D edgeMid = center + getHexEdgeMidpoint(e);
QPointF mapP2 = worldToMinimap(edgeMid.x(), edgeMid.y(), mapRect);
painter.drawLine(mapP1, mapP2);
}
}
}
}
// Draw unit dots on minimap
for (const auto &u : GameManager::instance().getUnits()) {
if (u->isDead()) continue;
@@ -725,9 +750,9 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
: Localization::instance().get("ui.unit.tip_build_city", "Found City (B)");
} else if (act == "boerderij_bouwen" || act == "build_farm") {
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement);
isActive = unit->isBuilding() || isQueued;
isActive = (unit->isBuilding() && unit->buildActionType != "road") || isQueued;
iconFile = "images/actions/farm.png";
if (unit->isBuilding()) {
if (unit->isBuilding() && unit->buildActionType != "road") {
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
tip = Localization::instance().get("ui.unit.tip_farm_building", "Busy building... %1%").arg(pct);
} else if (isQueued) {
@@ -735,6 +760,18 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
} else {
tip = Localization::instance().get("ui.unit.tip_build_farm", "Build Farm (F)");
}
} else if (act == "build_road" || act == "weg_bouwen") {
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildRoad);
isActive = (unit->isBuilding() && unit->buildActionType == "road") || isQueued;
iconFile = "images/actions/road.png";
if (unit->isBuilding() && unit->buildActionType == "road") {
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
tip = Localization::instance().get("ui.unit.tip_road_building", "Busy building road... %1%").arg(pct);
} else if (isQueued) {
tip = Localization::instance().get("ui.unit.tip_road_queued", "Build Road (Queued on arrival - click to cancel)");
} else {
tip = Localization::instance().get("ui.unit.tip_build_road", "Build Road (R)");
}
}
// Draw action bubble
@@ -945,12 +982,12 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
} else if (unit->hasQueuedAction()) {
QString qSummary = unit->getQueuedActionSummary();
QString queuedInfo = Localization::instance().get("ui.unit.status_queued", "Queued: %1").arg(qSummary);
if (unit->config.canBuildFarm() && !unit->config.infiniteCharges && unit->buildCharges > 0) {
if (unit->config.isBuilder() && !unit->config.infiniteCharges && unit->buildCharges > 0) {
queuedInfo += QString(" | ") + Localization::instance().get("ui.unit.charges_format", "Charges: %1").arg(unit->buildCharges);
}
painter.setPen(QColor(100, 220, 140));
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, queuedInfo);
} else if (unit->config.canBuildFarm() && !unit->config.infiniteCharges && unit->buildCharges > 0) {
} else if (unit->config.isBuilder() && !unit->config.infiniteCharges && unit->buildCharges > 0) {
QString builderInfo = Localization::instance().get("ui.unit.civilian_unit", "Civilian unit") + QString(" | ") +
Localization::instance().get("ui.unit.charges_format", "Charges: %1").arg(unit->buildCharges);
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, builderInfo);
@@ -998,9 +1035,9 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
: Localization::instance().get("ui.unit.tip_build_city", "Found City (B)");
} else if (act == "boerderij_bouwen" || act == "build_farm") {
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement);
isActive = unit->isBuilding() || isQueued;
isActive = (unit->isBuilding() && unit->buildActionType != "road") || isQueued;
iconFile = "images/actions/farm.png";
if (unit->isBuilding()) {
if (unit->isBuilding() && unit->buildActionType != "road") {
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
label = Localization::instance().get("ui.unit.btn_farm_building", "Building %1%").arg(pct);
tip = Localization::instance().get("ui.unit.tip_farm_building", "Busy building... %1%").arg(pct);
@@ -1011,6 +1048,21 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
label = Localization::instance().get("ui.unit.btn_build_farm", "Farm [F]");
tip = Localization::instance().get("ui.unit.tip_build_farm", "Build Farm (F)");
}
} else if (act == "build_road" || act == "weg_bouwen") {
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildRoad);
isActive = (unit->isBuilding() && unit->buildActionType == "road") || isQueued;
iconFile = "images/actions/road.png";
if (unit->isBuilding() && unit->buildActionType == "road") {
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
label = Localization::instance().get("ui.unit.btn_road_building", "Building %1%").arg(pct);
tip = Localization::instance().get("ui.unit.tip_road_building", "Busy building road... %1%").arg(pct);
} else if (isQueued) {
label = Localization::instance().get("ui.unit.btn_road_queued", "Road (Queued)");
tip = Localization::instance().get("ui.unit.tip_road_queued", "Build Road (Queued on arrival - click to cancel)");
} else {
label = Localization::instance().get("ui.unit.btn_build_road", "Road [R]");
tip = Localization::instance().get("ui.unit.tip_build_road", "Build Road (R)");
}
}
// Circular bubble button
@@ -1082,6 +1134,118 @@ void QSceneRenderer::drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatri
}
}
void QSceneRenderer::drawRoadsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
{
painter.setRenderHint(QPainter::Antialiasing, true);
const auto &roads = GameManager::instance().getTileRoads();
if (roads.empty()) return;
RoadConfigManager &roadConfigs = GameManager::instance().roadConfigs();
float meanderAmp = roadConfigs.meanderAmplitude();
int subdivisions = std::max(2, roadConfigs.meanderSubdivisions());
struct RoadBatch {
std::vector<QPolygonF> polylines;
std::vector<QPointF> centerJunctions;
std::vector<QPointF> isolatedCenters;
};
std::unordered_map<std::string, RoadBatch> batches;
for (const auto &pair : roads) {
const TileRoad &road = pair.second;
QVector2D center = Pathfinder::hexToWorld(road.tileX, road.tileY);
float centerZ = Unit::getTerrainHeight(center.x(), center.y(), m_point, m_numPoints05) + 0.028f;
bool cInFront = false;
QPointF cScreen = UnitRenderer::projectToScreen(QVector3D(center.x(), center.y(), centerZ), Projection, vMatrix, width(), height(), cInFront);
RoadBatch &batch = batches[road.roadTypeId];
if (road.connectionMask == 0) {
if (cInFront) {
batch.isolatedCenters.push_back(cScreen);
}
continue;
}
if (cInFront) {
batch.centerJunctions.push_back(cScreen);
}
for (int e = 0; e < 6; ++e) {
if (!road.hasConnection(e)) continue;
QVector2D endP = center + getHexEdgeMidpoint(e);
QVector2D dir = endP - center;
float len = dir.length();
QVector2D perp = (len > 0.0001f) ? QVector2D(-dir.y() / len, dir.x() / len) : QVector2D(0, 0);
// Deterministic pseudo-random seed for subtle, natural meandering
float seed = std::sin(float(road.tileX * 41 + road.tileY * 113 + e * 29)) * 43758.5453f;
float meanderSign = (seed - std::floor(seed) > 0.5f) ? 1.0f : -1.0f;
QPolygonF poly;
bool allVisible = true;
for (int i = 0; i <= subdivisions; ++i) {
float t = float(i) / float(subdivisions);
// Zero offset at both endpoints (t=0 and t=1) ensures perfect tile-to-tile alignment
float offset = std::sin(2.0f * 3.14159265f * t) * meanderAmp * meanderSign;
QVector2D p2d = center + dir * t + perp * offset;
float wz = Unit::getTerrainHeight(p2d.x(), p2d.y(), m_point, m_numPoints05) + 0.028f;
bool inFront = false;
QPointF sPos = UnitRenderer::projectToScreen(QVector3D(p2d.x(), p2d.y(), wz), Projection, vMatrix, width(), height(), inFront);
if (!inFront) {
allVisible = false;
break;
}
poly.append(sPos);
}
if (allVisible && poly.size() >= 2) {
batch.polylines.push_back(poly);
}
}
}
for (const auto &bPair : batches) {
const std::string &typeId = bPair.first;
const RoadBatch &batch = bPair.second;
const RoadTypeConfig *rCfg = roadConfigs.getRoadType(typeId);
QColor casingCol = rCfg ? rCfg->getBorderColor() : QColor(92, 64, 40);
float casingWidth = rCfg ? rCfg->borderWidth : 4.5f;
QColor surfaceCol = rCfg ? rCfg->getColor() : QColor(194, 166, 117);
float surfaceWidth = rCfg ? rCfg->width : 2.8f;
// Pass 1: Outer Casing / Under-layer
painter.setPen(QPen(casingCol, casingWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
for (const auto &poly : batch.polylines) {
painter.drawPolyline(poly);
}
// Pass 2: Road Surface Core
painter.setPen(QPen(surfaceCol, surfaceWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
for (const auto &poly : batch.polylines) {
painter.drawPolyline(poly);
}
// Pass 3: Center junction smooth cap
painter.setPen(Qt::NoPen);
painter.setBrush(surfaceCol);
for (const auto &cP : batch.centerJunctions) {
painter.drawEllipse(cP, surfaceWidth * 0.5f, surfaceWidth * 0.5f);
}
// Isolated road markers (tile has road, no connected neighbor yet)
for (const auto &cP : batch.isolatedCenters) {
painter.setBrush(surfaceCol);
painter.setPen(QPen(casingCol, 1.5f));
painter.drawEllipse(cP, surfaceWidth * 0.9f, surfaceWidth * 0.9f);
}
}
}
void QSceneRenderer::drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
{
painter.setRenderHint(QPainter::Antialiasing, true);
@@ -1115,7 +1279,7 @@ void QSceneRenderer::drawImprovementsOverlay(QPainter &painter, const QMatrix4x4
painter.drawRoundedRect(labelRect, 3, 3);
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
painter.setPen(QColor(255, 230, 130));
painter.drawText(labelRect, Qt::AlignCenter, "Boerderij");
painter.drawText(labelRect, Qt::AlignCenter, Localization::instance().get("ui.improvement.farm", "Farm"));
}
}
@@ -1873,6 +2037,8 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
GameManager::instance().commandBuildCitySelected(m_point, m_numPoints05);
} else if (ab.actionId == "boerderij_bouwen" || ab.actionId == "build_farm") {
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
} else if (ab.actionId == "build_road" || ab.actionId == "weg_bouwen") {
GameManager::instance().commandBuildRoadSelected(m_point, m_numPoints05);
}
update();
return;
@@ -2067,6 +2233,9 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e)
else if (e->key() == Qt::Key_F && sel && sel->config.canBuildFarm()) {
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
}
else if (e->key() == Qt::Key_R && sel && sel->config.canBuildRoad()) {
GameManager::instance().commandBuildRoadSelected(m_point, m_numPoints05);
}
else if (e->key() == Qt::Key_Escape) {
if (GameManager::instance().currentActionMode() != ActionMode::None) {
GameManager::instance().setActionMode(ActionMode::None);
@@ -2247,6 +2416,7 @@ void QSceneRenderer::paintGL()
// 3. Render 2D UI & HUD Overlays via QPainter
QPainter painter(this);
drawCityBorders(painter, vMatrix);
drawRoadsOverlay(painter, vMatrix);
drawImprovementsOverlay(painter, vMatrix);
if (!cityView) {
drawPathAndRange(painter, selected, vMatrix);