Implement builder road construction with hex-edge auto-connection, winding rendering, and speed bonus
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Road System Configuration
|
||||
default_road_type: "dirt"
|
||||
build_duration_ticks: 2
|
||||
meander_amplitude: 0.045
|
||||
meander_subdivisions: 5
|
||||
|
||||
types:
|
||||
dirt:
|
||||
id: "dirt"
|
||||
name: "Dirt Road"
|
||||
speed_multiplier: 1.30 # +30% movement speed
|
||||
color: "#c49a62"
|
||||
border_color: "#6b4b23"
|
||||
width: 3.5
|
||||
cost: 5
|
||||
stone:
|
||||
id: "stone"
|
||||
name: "Stone Road"
|
||||
speed_multiplier: 1.60 # +60% movement speed
|
||||
color: "#9aa1a8"
|
||||
border_color: "#464b50"
|
||||
width: 4.0
|
||||
cost: 15
|
||||
rail:
|
||||
id: "rail"
|
||||
name: "Railroad"
|
||||
speed_multiplier: 2.50 # +150% movement speed
|
||||
color: "#3a3c40"
|
||||
border_color: "#181a1c"
|
||||
width: 4.5
|
||||
cost: 30
|
||||
@@ -88,12 +88,27 @@ ui:
|
||||
btn_build_farm: "Farm [F]"
|
||||
btn_farm_queued: "Farm (Queued)"
|
||||
btn_farm_building: "Building %1%"
|
||||
btn_build_road: "Road [R]"
|
||||
btn_road_queued: "Road (Queued)"
|
||||
btn_road_building: "Building %1%"
|
||||
tip_move: "Move (M)"
|
||||
tip_attack: "Attack (A) [Range: %1]"
|
||||
tip_build_city: "Found City (B)"
|
||||
tip_build_farm: "Build Farm (F)"
|
||||
tip_farm_building: "Busy building... %1%"
|
||||
tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)"
|
||||
tip_build_road: "Build Road (R)"
|
||||
tip_road_building: "Busy building road... %1%"
|
||||
tip_road_queued: "Build Road (Queued on arrival - click to cancel)"
|
||||
road_started: "Constructing %1... (%2 ticks)"
|
||||
road_completed: "%1 completed!"
|
||||
road_queued: "Build Road queued on arrival"
|
||||
road_already_here: "Tile already has a road!"
|
||||
cannot_build_road_water: "Roads cannot be built on water!"
|
||||
|
||||
improvement:
|
||||
farm: "Farm"
|
||||
road: "Road"
|
||||
|
||||
city:
|
||||
center: "City Center"
|
||||
|
||||
@@ -12,5 +12,7 @@ icon: images/units/builder.png
|
||||
actions:
|
||||
- verplaatsen
|
||||
- boerderij_bouwen
|
||||
- build_road
|
||||
action_speeds:
|
||||
boerderij_bouwen: 10.0 # Duur in seconden die deze builder unit nodig heeft voor de actie boerderij_bouwen
|
||||
build_road: 2 # Duur in ticks
|
||||
|
||||
+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) {
|
||||
|
||||
+33
-1
@@ -12,13 +12,32 @@
|
||||
#include "matrix2d.h"
|
||||
#include "terrain.h"
|
||||
#include "terrainyieldmanager.h"
|
||||
#include "roadconfig.h"
|
||||
|
||||
enum class ActionMode {
|
||||
None,
|
||||
Move,
|
||||
Attack,
|
||||
BuildCity,
|
||||
BuildFarm
|
||||
BuildFarm,
|
||||
BuildRoad
|
||||
};
|
||||
|
||||
struct TileRoad {
|
||||
int tileX;
|
||||
int tileY;
|
||||
int playerId;
|
||||
QString roadTypeId; // "dirt", "stone", "rail"
|
||||
unsigned char connectionMask; // 6 bits: bit e (0..5) is 1 if connected to neighbor across edge e
|
||||
QVector3D worldPos;
|
||||
|
||||
bool hasConnection(int edge) const {
|
||||
return (connectionMask & (1 << (edge % 6))) != 0;
|
||||
}
|
||||
void setConnection(int edge, bool connected) {
|
||||
if (connected) connectionMask |= (1 << (edge % 6));
|
||||
else connectionMask &= ~(1 << (edge % 6));
|
||||
}
|
||||
};
|
||||
|
||||
struct TileImprovement {
|
||||
@@ -48,6 +67,8 @@ public:
|
||||
UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
|
||||
BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; }
|
||||
const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; }
|
||||
RoadConfigManager& roadConfigs() { return m_roadConfigManager; }
|
||||
const RoadConfigManager& roadConfigs() const { return m_roadConfigManager; }
|
||||
|
||||
const std::vector<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
|
||||
const std::vector<City>& getCities() const { return m_cities; }
|
||||
@@ -90,6 +111,7 @@ public:
|
||||
void commandAttack(Unit *target);
|
||||
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
void commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
void commandBuildRoadSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
void completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
void processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
|
||||
@@ -97,6 +119,14 @@ public:
|
||||
const TileImprovement* getImprovementAt(int tx, int ty) const;
|
||||
bool hasImprovementAt(int tx, int ty) const;
|
||||
|
||||
const std::map<std::pair<int, int>, TileRoad>& getTileRoads() const { return m_tileRoads; }
|
||||
const TileRoad* getRoadAt(int tx, int ty) const;
|
||||
bool hasRoadAt(int tx, int ty) const;
|
||||
void addRoad(int tx, int ty, int playerId, const QString &roadTypeId = "dirt", Matrix2D<TerrainPoint> *mapPoints = nullptr, int numPoints05 = 0);
|
||||
void removeRoad(int tx, int ty);
|
||||
void updateRoadConnections(int tx, int ty);
|
||||
float getRoadSpeedMultiplierBetween(int fromX, int fromY, int toX, int toY) const;
|
||||
|
||||
float getTickDuration() const { return m_movementTickDuration; }
|
||||
void setTickDuration(float d) { m_movementTickDuration = std::max(1.0f, d); }
|
||||
float getTickTimer() const { return m_tickTimer; }
|
||||
@@ -123,10 +153,12 @@ private:
|
||||
PlayerManager m_playerManager;
|
||||
UnitConfigManager m_unitConfigManager;
|
||||
BuildingConfigManager m_buildingConfigManager;
|
||||
RoadConfigManager m_roadConfigManager;
|
||||
std::vector<std::shared_ptr<Unit>> m_units;
|
||||
std::vector<City> m_cities;
|
||||
std::vector<FloatingText> m_floatingTexts;
|
||||
std::map<std::pair<int, int>, TileImprovement> m_tileImprovements;
|
||||
std::map<std::pair<int, int>, TileRoad> m_tileRoads;
|
||||
|
||||
int m_selectedUnitId;
|
||||
int m_activeCityViewId;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 962 B |
+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);
|
||||
|
||||
@@ -56,6 +56,7 @@ protected:
|
||||
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
// City & Territory Overlays
|
||||
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void drawRoadsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "roadconfig.h"
|
||||
#include "yamlparser.h"
|
||||
#include <QFileInfo>
|
||||
#include <QDebug>
|
||||
|
||||
RoadConfigManager::RoadConfigManager()
|
||||
: m_defaultRoadType("dirt"),
|
||||
m_buildDurationTicks(2),
|
||||
m_meanderAmplitude(0.045f),
|
||||
m_meanderSubdivisions(5)
|
||||
{
|
||||
// Provide built-in defaults
|
||||
RoadTypeConfig dirt;
|
||||
dirt.id = "dirt";
|
||||
dirt.name = "Dirt Road";
|
||||
dirt.speedMultiplier = 1.30f;
|
||||
dirt.colorStr = "#c49a62";
|
||||
dirt.borderColorStr = "#6b4b23";
|
||||
dirt.width = 3.5f;
|
||||
dirt.cost = 5;
|
||||
m_types["dirt"] = dirt;
|
||||
|
||||
RoadTypeConfig stone;
|
||||
stone.id = "stone";
|
||||
stone.name = "Stone Road";
|
||||
stone.speedMultiplier = 1.60f;
|
||||
stone.colorStr = "#9aa1a8";
|
||||
stone.borderColorStr = "#464b50";
|
||||
stone.width = 4.0f;
|
||||
stone.cost = 15;
|
||||
m_types["stone"] = stone;
|
||||
|
||||
RoadTypeConfig rail;
|
||||
rail.id = "rail";
|
||||
rail.name = "Railroad";
|
||||
rail.speedMultiplier = 2.50f;
|
||||
rail.colorStr = "#3a3c40";
|
||||
rail.borderColorStr = "#181a1c";
|
||||
rail.width = 4.5f;
|
||||
rail.cost = 30;
|
||||
m_types["rail"] = rail;
|
||||
}
|
||||
|
||||
bool RoadConfigManager::loadConfigFile(const QString &filePath)
|
||||
{
|
||||
YamlNode root = YamlParser::parseFile(filePath);
|
||||
if (root.children.isEmpty()) {
|
||||
qWarning() << "Failed to parse roads config YAML, using defaults:" << filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (root.hasChild("default_road_type")) {
|
||||
m_defaultRoadType = root["default_road_type"].asString("dirt");
|
||||
}
|
||||
if (root.hasChild("build_duration_ticks")) {
|
||||
m_buildDurationTicks = root["build_duration_ticks"].asInt(2);
|
||||
}
|
||||
if (root.hasChild("meander_amplitude")) {
|
||||
m_meanderAmplitude = root["meander_amplitude"].asFloat(0.045f);
|
||||
}
|
||||
if (root.hasChild("meander_subdivisions")) {
|
||||
m_meanderSubdivisions = root["meander_subdivisions"].asInt(5);
|
||||
}
|
||||
|
||||
if (root.hasChild("types")) {
|
||||
const YamlNode &typesNode = root["types"];
|
||||
for (auto it = typesNode.children.begin(); it != typesNode.children.end(); ++it) {
|
||||
RoadTypeConfig rCfg;
|
||||
rCfg.id = it.value().hasChild("id") ? it.value()["id"].asString() : it.key();
|
||||
rCfg.name = it.value().hasChild("name") ? it.value()["name"].asString() : rCfg.id;
|
||||
rCfg.speedMultiplier = it.value().hasChild("speed_multiplier") ? it.value()["speed_multiplier"].asFloat(1.30f) : 1.30f;
|
||||
rCfg.colorStr = it.value().hasChild("color") ? it.value()["color"].asString("#c49a62") : "#c49a62";
|
||||
rCfg.borderColorStr = it.value().hasChild("border_color") ? it.value()["border_color"].asString("#6b4b23") : "#6b4b23";
|
||||
rCfg.width = it.value().hasChild("width") ? it.value()["width"].asFloat(3.5f) : 3.5f;
|
||||
rCfg.cost = it.value().hasChild("cost") ? it.value()["cost"].asInt(5) : 5;
|
||||
m_types[rCfg.id] = rCfg;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const RoadTypeConfig* RoadConfigManager::getRoadType(const QString &id) const
|
||||
{
|
||||
auto it = m_types.find(id);
|
||||
if (it != m_types.end()) {
|
||||
return &it.value();
|
||||
}
|
||||
auto defIt = m_types.find(m_defaultRoadType);
|
||||
if (defIt != m_types.end()) {
|
||||
return &defIt.value();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef ROADCONFIG_H
|
||||
#define ROADCONFIG_H
|
||||
|
||||
#include <QString>
|
||||
#include <QMap>
|
||||
#include <QColor>
|
||||
|
||||
struct RoadTypeConfig {
|
||||
QString id = "dirt";
|
||||
QString name = "Dirt Road";
|
||||
float speedMultiplier = 1.30f;
|
||||
QString colorStr = "#c49a62";
|
||||
QString borderColorStr = "#6b4b23";
|
||||
float width = 3.5f;
|
||||
int cost = 5;
|
||||
|
||||
QColor getColor() const { return QColor(colorStr); }
|
||||
QColor getBorderColor() const { return QColor(borderColorStr); }
|
||||
};
|
||||
|
||||
class RoadConfigManager {
|
||||
public:
|
||||
RoadConfigManager();
|
||||
|
||||
bool loadConfigFile(const QString &filePath);
|
||||
const RoadTypeConfig* getRoadType(const QString &id) const;
|
||||
const QMap<QString, RoadTypeConfig>& getAllRoadTypes() const { return m_types; }
|
||||
|
||||
QString defaultRoadType() const { return m_defaultRoadType; }
|
||||
int buildDurationTicks() const { return m_buildDurationTicks; }
|
||||
float meanderAmplitude() const { return m_meanderAmplitude; }
|
||||
int meanderSubdivisions() const { return m_meanderSubdivisions; }
|
||||
|
||||
private:
|
||||
QString m_defaultRoadType;
|
||||
int m_buildDurationTicks;
|
||||
float m_meanderAmplitude;
|
||||
int m_meanderSubdivisions;
|
||||
QMap<QString, RoadTypeConfig> m_types;
|
||||
};
|
||||
|
||||
#endif // ROADCONFIG_H
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "unitconfig.h"
|
||||
#include "pathfinder.h"
|
||||
#include "yamlparser.h"
|
||||
#include "gamemanager.h"
|
||||
#include <QDebug>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
@@ -300,15 +301,22 @@ float TerrainMovementManager::calculateStepCost(
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
float cost = baseCost;
|
||||
if (deltaH > 0.001f) {
|
||||
// Uphill penalty: going uphill increases movement cost
|
||||
float penalty = deltaH * m_config.uphillSlopeMultiplier;
|
||||
return baseCost + penalty;
|
||||
cost = baseCost + penalty;
|
||||
} else if (deltaH < -0.001f) {
|
||||
// Downhill bonus: going downhill decreases movement cost (negative penalty)
|
||||
float bonus = (-deltaH) * m_config.downhillSlopeBonusMultiplier;
|
||||
return std::max(m_config.minStepCost, baseCost - bonus);
|
||||
cost = std::max(m_config.minStepCost, baseCost - bonus);
|
||||
}
|
||||
|
||||
return baseCost;
|
||||
// Road speed bonus: if traveling between connected road tiles, movement speed increases!
|
||||
float roadMult = GameManager::instance().getRoadSpeedMultiplierBetween(fromX, fromY, toX, toY);
|
||||
if (roadMult > 1.0f) {
|
||||
cost /= roadMult;
|
||||
}
|
||||
|
||||
return std::max(0.1f, cost);
|
||||
}
|
||||
|
||||
+4
-2
@@ -31,7 +31,8 @@ SOURCES += main.cpp \
|
||||
gamemanager.cpp \
|
||||
unitrenderer.cpp \
|
||||
localization.cpp \
|
||||
terrainmovementmanager.cpp
|
||||
terrainmovementmanager.cpp \
|
||||
roadconfig.cpp
|
||||
|
||||
HEADERS += mainwindow.h \
|
||||
qscenerenderer.h \
|
||||
@@ -52,7 +53,8 @@ HEADERS += mainwindow.h \
|
||||
gamemanager.h \
|
||||
unitrenderer.h \
|
||||
localization.h \
|
||||
terrainmovementmanager.h
|
||||
terrainmovementmanager.h \
|
||||
roadconfig.h
|
||||
|
||||
FORMS += mainwindow.ui
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ QString Unit::getQueuedActionSummary() const
|
||||
return "Build Farm";
|
||||
case UnitActionType::BuildCity:
|
||||
return "Found City";
|
||||
case UnitActionType::BuildRoad:
|
||||
return "Build Road";
|
||||
case UnitActionType::Attack:
|
||||
return "Attack";
|
||||
default:
|
||||
|
||||
@@ -22,6 +22,7 @@ enum class UnitActionType {
|
||||
Move,
|
||||
BuildImprovement,
|
||||
BuildCity,
|
||||
BuildRoad,
|
||||
Attack
|
||||
};
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ struct UnitConfig {
|
||||
bool canAttack() const { return actions.contains("aanvallen") || isMilitary(); }
|
||||
bool canBuildCity() const { return actions.contains("steden_bouwen") || actions.contains("build_city"); }
|
||||
bool canBuildFarm() const { return actions.contains("boerderij_bouwen") || actions.contains("build_farm"); }
|
||||
bool canBuildRoad() const { return actions.contains("build_road") || actions.contains("weg_bouwen"); }
|
||||
bool isBuilder() const { return canBuildFarm() || canBuildRoad(); }
|
||||
|
||||
float getActionDuration(const QString &actionName, float fallback = 10.0f) const {
|
||||
if (actionSpeeds.contains(actionName)) {
|
||||
|
||||
Reference in New Issue
Block a user