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_build_farm: "Farm [F]"
|
||||||
btn_farm_queued: "Farm (Queued)"
|
btn_farm_queued: "Farm (Queued)"
|
||||||
btn_farm_building: "Building %1%"
|
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_move: "Move (M)"
|
||||||
tip_attack: "Attack (A) [Range: %1]"
|
tip_attack: "Attack (A) [Range: %1]"
|
||||||
tip_build_city: "Found City (B)"
|
tip_build_city: "Found City (B)"
|
||||||
tip_build_farm: "Build Farm (F)"
|
tip_build_farm: "Build Farm (F)"
|
||||||
tip_farm_building: "Busy building... %1%"
|
tip_farm_building: "Busy building... %1%"
|
||||||
tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)"
|
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:
|
city:
|
||||||
center: "City Center"
|
center: "City Center"
|
||||||
|
|||||||
@@ -12,5 +12,7 @@ icon: images/units/builder.png
|
|||||||
actions:
|
actions:
|
||||||
- verplaatsen
|
- verplaatsen
|
||||||
- boerderij_bouwen
|
- boerderij_bouwen
|
||||||
|
- build_road
|
||||||
action_speeds:
|
action_speeds:
|
||||||
boerderij_bouwen: 10.0 # Duur in seconden die deze builder unit nodig heeft voor de actie boerderij_bouwen
|
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";
|
QString buildingsConfigPath = baseDir + "/config/buildings.yaml";
|
||||||
m_buildingConfigManager.loadConfigFile(buildingsConfigPath);
|
m_buildingConfigManager.loadConfigFile(buildingsConfigPath);
|
||||||
|
|
||||||
|
// Load road YAML configurations
|
||||||
|
QString roadsConfigPath = baseDir + "/config/roads.yaml";
|
||||||
|
m_roadConfigManager.loadConfigFile(roadsConfigPath);
|
||||||
|
|
||||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||||
|
|
||||||
// Initialize player national resources from configuration
|
// 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();
|
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)
|
void GameManager::commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||||
{
|
{
|
||||||
Unit *selected = getSelectedUnit();
|
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);
|
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)
|
void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||||
{
|
{
|
||||||
int tx = unit.tileX;
|
int tx = unit.tileX;
|
||||||
int ty = unit.tileY;
|
int ty = unit.tileY;
|
||||||
QString impId = unit.buildingImprovementId.isEmpty() ? "farm" : unit.buildingImprovementId;
|
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)) {
|
if (hasImprovementAt(tx, ty)) {
|
||||||
unit.cancelBuilding();
|
unit.cancelBuilding();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place improvement now that it is completed!
|
// 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;
|
TileImprovement imp;
|
||||||
imp.improvementId = impId;
|
imp.improvementId = impId;
|
||||||
imp.tileX = tx;
|
imp.tileX = tx;
|
||||||
@@ -923,6 +1100,31 @@ void GameManager::processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *map
|
|||||||
}
|
}
|
||||||
break;
|
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: {
|
case UnitActionType::Attack: {
|
||||||
Unit *target = nullptr;
|
Unit *target = nullptr;
|
||||||
for (auto &other : m_units) {
|
for (auto &other : m_units) {
|
||||||
|
|||||||
+33
-1
@@ -12,13 +12,32 @@
|
|||||||
#include "matrix2d.h"
|
#include "matrix2d.h"
|
||||||
#include "terrain.h"
|
#include "terrain.h"
|
||||||
#include "terrainyieldmanager.h"
|
#include "terrainyieldmanager.h"
|
||||||
|
#include "roadconfig.h"
|
||||||
|
|
||||||
enum class ActionMode {
|
enum class ActionMode {
|
||||||
None,
|
None,
|
||||||
Move,
|
Move,
|
||||||
Attack,
|
Attack,
|
||||||
BuildCity,
|
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 {
|
struct TileImprovement {
|
||||||
@@ -48,6 +67,8 @@ public:
|
|||||||
UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
|
UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
|
||||||
BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; }
|
BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; }
|
||||||
const BuildingConfigManager& buildingConfigs() const { 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<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
|
||||||
const std::vector<City>& getCities() const { return m_cities; }
|
const std::vector<City>& getCities() const { return m_cities; }
|
||||||
@@ -90,6 +111,7 @@ public:
|
|||||||
void commandAttack(Unit *target);
|
void commandAttack(Unit *target);
|
||||||
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||||
void commandBuildFarmSelected(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 completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||||
void processUnitActionQueue(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;
|
const TileImprovement* getImprovementAt(int tx, int ty) const;
|
||||||
bool hasImprovementAt(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; }
|
float getTickDuration() const { return m_movementTickDuration; }
|
||||||
void setTickDuration(float d) { m_movementTickDuration = std::max(1.0f, d); }
|
void setTickDuration(float d) { m_movementTickDuration = std::max(1.0f, d); }
|
||||||
float getTickTimer() const { return m_tickTimer; }
|
float getTickTimer() const { return m_tickTimer; }
|
||||||
@@ -123,10 +153,12 @@ private:
|
|||||||
PlayerManager m_playerManager;
|
PlayerManager m_playerManager;
|
||||||
UnitConfigManager m_unitConfigManager;
|
UnitConfigManager m_unitConfigManager;
|
||||||
BuildingConfigManager m_buildingConfigManager;
|
BuildingConfigManager m_buildingConfigManager;
|
||||||
|
RoadConfigManager m_roadConfigManager;
|
||||||
std::vector<std::shared_ptr<Unit>> m_units;
|
std::vector<std::shared_ptr<Unit>> m_units;
|
||||||
std::vector<City> m_cities;
|
std::vector<City> m_cities;
|
||||||
std::vector<FloatingText> m_floatingTexts;
|
std::vector<FloatingText> m_floatingTexts;
|
||||||
std::map<std::pair<int, int>, TileImprovement> m_tileImprovements;
|
std::map<std::pair<int, int>, TileImprovement> m_tileImprovements;
|
||||||
|
std::map<std::pair<int, int>, TileRoad> m_tileRoads;
|
||||||
|
|
||||||
int m_selectedUnitId;
|
int m_selectedUnitId;
|
||||||
int m_activeCityViewId;
|
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.setPen(QPen(QColor(120, 150, 180, 160), 1.0));
|
||||||
painter.drawRect(mapRect);
|
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
|
// Draw unit dots on minimap
|
||||||
for (const auto &u : GameManager::instance().getUnits()) {
|
for (const auto &u : GameManager::instance().getUnits()) {
|
||||||
if (u->isDead()) continue;
|
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)");
|
: Localization::instance().get("ui.unit.tip_build_city", "Found City (B)");
|
||||||
} else if (act == "boerderij_bouwen" || act == "build_farm") {
|
} else if (act == "boerderij_bouwen" || act == "build_farm") {
|
||||||
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement);
|
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement);
|
||||||
isActive = unit->isBuilding() || isQueued;
|
isActive = (unit->isBuilding() && unit->buildActionType != "road") || isQueued;
|
||||||
iconFile = "images/actions/farm.png";
|
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);
|
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);
|
tip = Localization::instance().get("ui.unit.tip_farm_building", "Busy building... %1%").arg(pct);
|
||||||
} else if (isQueued) {
|
} else if (isQueued) {
|
||||||
@@ -735,6 +760,18 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
|
|||||||
} else {
|
} else {
|
||||||
tip = Localization::instance().get("ui.unit.tip_build_farm", "Build 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);
|
||||||
|
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
|
// Draw action bubble
|
||||||
@@ -945,12 +982,12 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
|
|||||||
} else if (unit->hasQueuedAction()) {
|
} else if (unit->hasQueuedAction()) {
|
||||||
QString qSummary = unit->getQueuedActionSummary();
|
QString qSummary = unit->getQueuedActionSummary();
|
||||||
QString queuedInfo = Localization::instance().get("ui.unit.status_queued", "Queued: %1").arg(qSummary);
|
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);
|
queuedInfo += QString(" | ") + Localization::instance().get("ui.unit.charges_format", "Charges: %1").arg(unit->buildCharges);
|
||||||
}
|
}
|
||||||
painter.setPen(QColor(100, 220, 140));
|
painter.setPen(QColor(100, 220, 140));
|
||||||
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, queuedInfo);
|
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(" | ") +
|
QString builderInfo = Localization::instance().get("ui.unit.civilian_unit", "Civilian unit") + QString(" | ") +
|
||||||
Localization::instance().get("ui.unit.charges_format", "Charges: %1").arg(unit->buildCharges);
|
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);
|
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)");
|
: Localization::instance().get("ui.unit.tip_build_city", "Found City (B)");
|
||||||
} else if (act == "boerderij_bouwen" || act == "build_farm") {
|
} else if (act == "boerderij_bouwen" || act == "build_farm") {
|
||||||
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement);
|
bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement);
|
||||||
isActive = unit->isBuilding() || isQueued;
|
isActive = (unit->isBuilding() && unit->buildActionType != "road") || isQueued;
|
||||||
iconFile = "images/actions/farm.png";
|
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);
|
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
|
||||||
label = Localization::instance().get("ui.unit.btn_farm_building", "Building %1%").arg(pct);
|
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);
|
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]");
|
label = Localization::instance().get("ui.unit.btn_build_farm", "Farm [F]");
|
||||||
tip = Localization::instance().get("ui.unit.tip_build_farm", "Build 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
|
// 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)
|
void QSceneRenderer::drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
|
||||||
{
|
{
|
||||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
@@ -1115,7 +1279,7 @@ void QSceneRenderer::drawImprovementsOverlay(QPainter &painter, const QMatrix4x4
|
|||||||
painter.drawRoundedRect(labelRect, 3, 3);
|
painter.drawRoundedRect(labelRect, 3, 3);
|
||||||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||||||
painter.setPen(QColor(255, 230, 130));
|
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);
|
GameManager::instance().commandBuildCitySelected(m_point, m_numPoints05);
|
||||||
} else if (ab.actionId == "boerderij_bouwen" || ab.actionId == "build_farm") {
|
} else if (ab.actionId == "boerderij_bouwen" || ab.actionId == "build_farm") {
|
||||||
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
|
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();
|
update();
|
||||||
return;
|
return;
|
||||||
@@ -2067,6 +2233,9 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e)
|
|||||||
else if (e->key() == Qt::Key_F && sel && sel->config.canBuildFarm()) {
|
else if (e->key() == Qt::Key_F && sel && sel->config.canBuildFarm()) {
|
||||||
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
|
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) {
|
else if (e->key() == Qt::Key_Escape) {
|
||||||
if (GameManager::instance().currentActionMode() != ActionMode::None) {
|
if (GameManager::instance().currentActionMode() != ActionMode::None) {
|
||||||
GameManager::instance().setActionMode(ActionMode::None);
|
GameManager::instance().setActionMode(ActionMode::None);
|
||||||
@@ -2247,6 +2416,7 @@ void QSceneRenderer::paintGL()
|
|||||||
// 3. Render 2D UI & HUD Overlays via QPainter
|
// 3. Render 2D UI & HUD Overlays via QPainter
|
||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
drawCityBorders(painter, vMatrix);
|
drawCityBorders(painter, vMatrix);
|
||||||
|
drawRoadsOverlay(painter, vMatrix);
|
||||||
drawImprovementsOverlay(painter, vMatrix);
|
drawImprovementsOverlay(painter, vMatrix);
|
||||||
if (!cityView) {
|
if (!cityView) {
|
||||||
drawPathAndRange(painter, selected, vMatrix);
|
drawPathAndRange(painter, selected, vMatrix);
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ protected:
|
|||||||
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
|
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||||
// City & Territory Overlays
|
// City & Territory Overlays
|
||||||
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
|
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||||
|
void drawRoadsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||||
void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||||
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||||
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
|
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 "unitconfig.h"
|
||||||
#include "pathfinder.h"
|
#include "pathfinder.h"
|
||||||
#include "yamlparser.h"
|
#include "yamlparser.h"
|
||||||
|
#include "gamemanager.h"
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -300,15 +301,22 @@ float TerrainMovementManager::calculateStepCost(
|
|||||||
return -1.0f;
|
return -1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float cost = baseCost;
|
||||||
if (deltaH > 0.001f) {
|
if (deltaH > 0.001f) {
|
||||||
// Uphill penalty: going uphill increases movement cost
|
// Uphill penalty: going uphill increases movement cost
|
||||||
float penalty = deltaH * m_config.uphillSlopeMultiplier;
|
float penalty = deltaH * m_config.uphillSlopeMultiplier;
|
||||||
return baseCost + penalty;
|
cost = baseCost + penalty;
|
||||||
} else if (deltaH < -0.001f) {
|
} else if (deltaH < -0.001f) {
|
||||||
// Downhill bonus: going downhill decreases movement cost (negative penalty)
|
// Downhill bonus: going downhill decreases movement cost (negative penalty)
|
||||||
float bonus = (-deltaH) * m_config.downhillSlopeBonusMultiplier;
|
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 \
|
gamemanager.cpp \
|
||||||
unitrenderer.cpp \
|
unitrenderer.cpp \
|
||||||
localization.cpp \
|
localization.cpp \
|
||||||
terrainmovementmanager.cpp
|
terrainmovementmanager.cpp \
|
||||||
|
roadconfig.cpp
|
||||||
|
|
||||||
HEADERS += mainwindow.h \
|
HEADERS += mainwindow.h \
|
||||||
qscenerenderer.h \
|
qscenerenderer.h \
|
||||||
@@ -52,7 +53,8 @@ HEADERS += mainwindow.h \
|
|||||||
gamemanager.h \
|
gamemanager.h \
|
||||||
unitrenderer.h \
|
unitrenderer.h \
|
||||||
localization.h \
|
localization.h \
|
||||||
terrainmovementmanager.h
|
terrainmovementmanager.h \
|
||||||
|
roadconfig.h
|
||||||
|
|
||||||
FORMS += mainwindow.ui
|
FORMS += mainwindow.ui
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ QString Unit::getQueuedActionSummary() const
|
|||||||
return "Build Farm";
|
return "Build Farm";
|
||||||
case UnitActionType::BuildCity:
|
case UnitActionType::BuildCity:
|
||||||
return "Found City";
|
return "Found City";
|
||||||
|
case UnitActionType::BuildRoad:
|
||||||
|
return "Build Road";
|
||||||
case UnitActionType::Attack:
|
case UnitActionType::Attack:
|
||||||
return "Attack";
|
return "Attack";
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ enum class UnitActionType {
|
|||||||
Move,
|
Move,
|
||||||
BuildImprovement,
|
BuildImprovement,
|
||||||
BuildCity,
|
BuildCity,
|
||||||
|
BuildRoad,
|
||||||
Attack
|
Attack
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ struct UnitConfig {
|
|||||||
bool canAttack() const { return actions.contains("aanvallen") || isMilitary(); }
|
bool canAttack() const { return actions.contains("aanvallen") || isMilitary(); }
|
||||||
bool canBuildCity() const { return actions.contains("steden_bouwen") || actions.contains("build_city"); }
|
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 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 {
|
float getActionDuration(const QString &actionName, float fallback = 10.0f) const {
|
||||||
if (actionSpeeds.contains(actionName)) {
|
if (actionSpeeds.contains(actionName)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user