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
+205 -3
View File
@@ -54,6 +54,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
QString buildingsConfigPath = baseDir + "/config/buildings.yaml";
m_buildingConfigManager.loadConfigFile(buildingsConfigPath);
// Load road YAML configurations
QString roadsConfigPath = baseDir + "/config/roads.yaml";
m_roadConfigManager.loadConfigFile(roadsConfigPath);
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
// Initialize player national resources from configuration
@@ -667,6 +671,89 @@ bool GameManager::hasImprovementAt(int tx, int ty) const
return m_tileImprovements.find({tx, ty}) != m_tileImprovements.end();
}
const TileRoad* GameManager::getRoadAt(int tx, int ty) const
{
auto it = m_tileRoads.find({tx, ty});
if (it != m_tileRoads.end()) {
return &it->second;
}
return nullptr;
}
bool GameManager::hasRoadAt(int tx, int ty) const
{
return m_tileRoads.find({tx, ty}) != m_tileRoads.end();
}
void GameManager::updateRoadConnections(int tx, int ty)
{
auto it = m_tileRoads.find({tx, ty});
if (it == m_tileRoads.end()) return;
unsigned char mask = 0;
for (int e = 0; e < 6; ++e) {
int nx, ny;
getHexNeighbor(tx, ty, e, nx, ny);
if (nx >= 0 && nx < MAP_WIDTH && ny >= 0 && ny < MAP_HEIGHT) {
auto nIt = m_tileRoads.find({nx, ny});
if (nIt != m_tileRoads.end()) {
mask |= (1 << e);
// Also update the neighbor's opposite edge (e + 3) % 6
nIt->second.connectionMask |= (1 << ((e + 3) % 6));
}
}
}
it->second.connectionMask = mask;
}
void GameManager::addRoad(int tx, int ty, int playerId, const QString &roadTypeId, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
QVector2D w = Pathfinder::hexToWorld(tx, ty);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
TileRoad road;
road.roadTypeId = roadTypeId.isEmpty() ? m_roadConfigManager.defaultRoadType() : roadTypeId;
road.tileX = tx;
road.tileY = ty;
road.playerId = playerId;
road.worldPos = QVector3D(w.x(), w.y(), gz);
road.connectionMask = 0;
m_tileRoads[{tx, ty}] = road;
updateRoadConnections(tx, ty);
}
void GameManager::removeRoad(int tx, int ty)
{
auto it = m_tileRoads.find({tx, ty});
if (it == m_tileRoads.end()) return;
for (int e = 0; e < 6; ++e) {
int nx, ny;
getHexNeighbor(tx, ty, e, nx, ny);
auto nIt = m_tileRoads.find({nx, ny});
if (nIt != m_tileRoads.end()) {
nIt->second.connectionMask &= ~(1 << ((e + 3) % 6));
}
}
m_tileRoads.erase(it);
}
float GameManager::getRoadSpeedMultiplierBetween(int fromX, int fromY, int toX, int toY) const
{
auto itFrom = m_tileRoads.find({fromX, fromY});
auto itTo = m_tileRoads.find({toX, toY});
if (itFrom != m_tileRoads.end() && itTo != m_tileRoads.end()) {
const RoadTypeConfig *cfgFrom = m_roadConfigManager.getRoadType(itFrom->second.roadTypeId);
const RoadTypeConfig *cfgTo = m_roadConfigManager.getRoadType(itTo->second.roadTypeId);
float multFrom = cfgFrom ? cfgFrom->speedMultiplier : 1.30f;
float multTo = cfgTo ? cfgTo->speedMultiplier : 1.30f;
return std::min(multFrom, multTo);
}
return 1.0f;
}
void GameManager::commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
@@ -754,21 +841,111 @@ void GameManager::commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, in
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
}
void GameManager::commandBuildRoadSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
if (!selected || !selected->config.canBuildRoad()) return;
if (selected->isBuilding()) {
addFloatingText(Localization::instance().get("ui.unit.already_building", "Already busy building!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 200, 70));
return;
}
QString defaultType = m_roadConfigManager.defaultRoadType();
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType);
QString roadName = rCfg ? rCfg->name : "Road";
// If unit is currently moving: toggle or queue build road action on arrival!
if (selected->state == UnitState::Moving) {
if (selected->hasQueuedActionType(UnitActionType::BuildRoad)) {
selected->removeQueuedActionsOfType(UnitActionType::BuildRoad);
addFloatingText(Localization::instance().get("ui.unit.action_cancelled", "%1 cancelled").arg("Build Road"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80));
return;
}
int destX = selected->targetTileX;
int destY = selected->targetTileY;
if (hasRoadAt(destX, destY)) {
addFloatingText(Localization::instance().get("ui.unit.road_already_here", "Tile already has a road!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
if (!TerrainMovementManager::instance().isHexPassable(
destX, destY, MovementDomain::Land, mapPoints, numPoints05, &selected->config)) {
addFloatingText(Localization::instance().get("ui.unit.cannot_build_road_water", "Roads cannot be built on water!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
if (selected->queueAction(UnitAction(UnitActionType::BuildRoad, destX, destY, defaultType))) {
addFloatingText(Localization::instance().get("ui.unit.road_queued", "Build Road queued on arrival"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(80, 220, 120), 2.2f);
} else {
addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
}
return;
}
int tx = selected->tileX;
int ty = selected->tileY;
if (hasRoadAt(tx, ty)) {
addFloatingText(Localization::instance().get("ui.unit.road_already_here", "Tile already has a road!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
if (!TerrainMovementManager::instance().isHexPassable(
tx, ty, MovementDomain::Land, mapPoints, numPoints05, &selected->config)) {
addFloatingText(Localization::instance().get("ui.unit.cannot_build_road_water", "Roads cannot be built on water!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
// Duration is 2 ticks (configured via YAML)
int ticks = m_roadConfigManager.buildDurationTicks();
float duration = (float)ticks * m_movementTickDuration;
selected->startBuilding(QString("road:%1").arg(defaultType), duration);
addFloatingText(Localization::instance().get("ui.unit.road_started", "Constructing %1... (%2 ticks)").arg(roadName).arg(ticks),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
}
void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
int tx = unit.tileX;
int ty = unit.tileY;
QString impId = unit.buildingImprovementId.isEmpty() ? "farm" : unit.buildingImprovementId;
QVector2D w = Pathfinder::hexToWorld(tx, ty);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
// Check if completing a road
if (impId.startsWith("road")) {
QString roadType = "dirt";
if (impId.contains(":")) {
roadType = impId.section(":", 1);
}
addRoad(tx, ty, unit.playerId, roadType, mapPoints, numPoints05);
unit.cancelBuilding();
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(roadType);
QString roadName = rCfg ? rCfg->name : "Road";
addFloatingText(Localization::instance().get("ui.unit.road_completed", "%1 completed!").arg(roadName),
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(240, 210, 100), 2.8f);
return;
}
if (hasImprovementAt(tx, ty)) {
unit.cancelBuilding();
return;
}
// Place improvement now that it is completed!
QVector2D w = Pathfinder::hexToWorld(tx, ty);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
TileImprovement imp;
imp.improvementId = impId;
imp.tileX = tx;
@@ -923,6 +1100,31 @@ void GameManager::processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *map
}
break;
}
case UnitActionType::BuildRoad: {
int tx = unit.tileX;
int ty = unit.tileY;
if (hasRoadAt(tx, ty)) {
addFloatingText(Localization::instance().get("ui.unit.road_already_here", "Tile already has a road!"),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
if (!TerrainMovementManager::instance().isHexPassable(
tx, ty, MovementDomain::Land, mapPoints, numPoints05, &unit.config)) {
addFloatingText(Localization::instance().get("ui.unit.cannot_build_road_water", "Roads cannot be built on water!"),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
QString roadType = action.extraData.isEmpty() ? m_roadConfigManager.defaultRoadType() : action.extraData;
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(roadType);
QString roadName = rCfg ? rCfg->name : "Road";
int ticks = m_roadConfigManager.buildDurationTicks();
float duration = (float)ticks * m_movementTickDuration;
unit.startBuilding(QString("road:%1").arg(roadType), duration);
addFloatingText(Localization::instance().get("ui.unit.road_started", "Constructing %1... (%2 ticks)").arg(roadName).arg(ticks),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
break;
}
case UnitActionType::Attack: {
Unit *target = nullptr;
for (auto &other : m_units) {