Implement builder road construction with hex-edge auto-connection, winding rendering, and speed bonus
This commit is contained in:
+177
-7
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user