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