Files
Ascent-of-Man/gamemanager.cpp
T

1267 lines
51 KiB
C++

#include "gamemanager.h"
#include "pathfinder.h"
#include "yamlparser.h"
#include "terrainyieldmanager.h"
#include "terrainmovementmanager.h"
#include "localization.h"
#include <QDebug>
#include <cmath>
GameManager& GameManager::instance()
{
static GameManager s_instance;
return s_instance;
}
GameManager::GameManager()
: m_selectedUnitId(-1), m_activeCityViewId(-1), m_actionMode(ActionMode::None),
m_movementTickDuration(30.0f), m_tickTimer(0.0f),
m_simSpeedMultiplier(1.0f),
m_maxActionQueueSize(5), m_autoQueueMoveWhenBusy(true), m_allowActionQueueing(true),
m_nextUnitId(0), m_nextCityId(0),
m_initialized(false)
{
}
void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
if (m_initialized) return;
// Load localization
Localization::instance().loadFile(baseDir + "/config/strings_en.yaml");
// Load terrain yields & city configuration
TerrainYieldManager::instance().loadConfigs(baseDir);
// Load game settings
QString gameConfigPath = baseDir + "/config/game.yaml";
YamlNode gameRoot = YamlParser::parseFile(gameConfigPath);
if (gameRoot.hasChild("movement_tick_duration")) {
m_movementTickDuration = gameRoot["movement_tick_duration"].asFloat(30.0f);
}
if (gameRoot.hasChild("action_queue")) {
const YamlNode &aqNode = gameRoot["action_queue"];
m_maxActionQueueSize = aqNode["max_size"].asInt(5);
m_autoQueueMoveWhenBusy = aqNode["auto_queue_move_when_busy"].asBool(true);
m_allowActionQueueing = aqNode["allow_action_queueing"].asBool(true);
}
// Load unit YAML configurations
QString unitsConfigDir = baseDir + "/config/units";
m_unitConfigManager.loadFromDirectory(unitsConfigDir);
// Load building & district YAML configurations
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
for (auto &player : m_playerManager.getPlayersMutable()) {
player.culture = cCfg.initialNationalResources.value("cultuur", 10000);
player.science = cCfg.initialNationalResources.value("kennis", 0);
player.faith = cCfg.initialNationalResources.value("geloof", 0);
player.gold = cCfg.initialNationalResources.value("goud", 0);
}
// Spawn an initial established city for Player 1 at (35, 17)
int cId = getNextCityId();
QVector2D w = Pathfinder::hexToWorld(35, 17);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
City initialCity(cId, 1, "Roma", 35, 17, QVector3D(w.x(), w.y(), gz),
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
// Assign initial 1 pop to work the city center tile with best yield resource
QString defRes = getBestTileResource(35, 17, mapPoints, numPoints05);
initialCity.assignWorker(35, 17, defRes);
m_cities.push_back(initialCity);
// Spawn initial demo units near starting camera position
// Player 1 (Blue & Gold)
spawnUnit("settler", 1, 36, 18, mapPoints, numPoints05);
spawnUnit("builder", 1, 35, 18, mapPoints, numPoints05);
spawnUnit("warrior", 1, 37, 18, mapPoints, numPoints05);
spawnUnit("slinger", 1, 37, 19, mapPoints, numPoints05);
spawnUnit("galley", 1, 41, 92, mapPoints, numPoints05);
// Player 2 (Red & White) - Opponent units
spawnUnit("warrior", 2, 40, 18, mapPoints, numPoints05);
spawnUnit("slinger", 2, 41, 19, mapPoints, numPoints05);
m_initialized = true;
qDebug() << "GameManager initialized with" << m_cities.size() << "cities and" << m_units.size() << "units.";
}
Unit* GameManager::spawnUnit(const QString &configId, int playerId, int tileX, int tileY,
Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
const UnitConfig *cfg = m_unitConfigManager.getConfig(configId);
if (!cfg) {
qWarning() << "Unit config not found:" << configId;
return nullptr;
}
QVector2D w = Pathfinder::hexToWorld(tileX, tileY);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
int id = getNextUnitId();
auto unit = std::make_shared<Unit>(id, playerId, *cfg, tileX, tileY, gz);
unit->maxQueueSize = m_maxActionQueueSize;
m_units.push_back(unit);
return unit.get();
}
void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
// Update movement tick timer
m_tickTimer += deltaTime * m_simSpeedMultiplier;
if (m_tickTimer >= m_movementTickDuration) {
m_tickTimer = std::fmod(m_tickTimer, m_movementTickDuration);
processTickYields(mapPoints, numPoints05);
}
// Update units
for (auto &u : m_units) {
if (!u->isDead()) {
u->update(deltaTime, m_movementTickDuration, m_simSpeedMultiplier, mapPoints, numPoints05);
if (u->isBuilding() && u->buildProgress >= 1.0f) {
completeUnitBuilding(*u, mapPoints, numPoints05);
}
if (!u->isDead() && u->state == UnitState::Idle && u->hasQueuedAction()) {
processUnitActionQueue(*u, mapPoints, numPoints05);
}
}
}
// Clean up dead units
for (auto it = m_units.begin(); it != m_units.end(); ) {
if ((*it)->isDead()) {
if ((*it)->id == m_selectedUnitId) {
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
it = m_units.erase(it);
} else {
++it;
}
}
// Update floating texts
for (auto it = m_floatingTexts.begin(); it != m_floatingTexts.end(); ) {
it->life -= deltaTime;
it->worldPos.setZ(it->worldPos.z() + deltaTime * 0.4f);
if (it->life <= 0.0f) {
it = m_floatingTexts.erase(it);
} else {
++it;
}
}
}
Unit* GameManager::getSelectedUnit()
{
return getUnitById(m_selectedUnitId);
}
const Unit* GameManager::getSelectedUnit() const
{
for (const auto &u : m_units) {
if (u->id == m_selectedUnitId) return u.get();
}
return nullptr;
}
void GameManager::selectUnit(int id)
{
m_selectedUnitId = id;
m_activeCityViewId = -1; // Close city view when unit is selected
m_actionMode = ActionMode::None;
}
void GameManager::deselectUnit()
{
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
Unit* GameManager::getUnitAt(int tx, int ty)
{
for (auto &u : m_units) {
if (!u->isDead() && u->tileX == tx && u->tileY == ty) {
return u.get();
}
}
return nullptr;
}
Unit* GameManager::getUnitById(int id)
{
for (auto &u : m_units) {
if (u->id == id && !u->isDead()) return u.get();
}
return nullptr;
}
City* GameManager::getCityAt(int tx, int ty)
{
for (auto &c : m_cities) {
if (c.tileX == tx && c.tileY == ty) return &c;
}
return nullptr;
}
City* GameManager::getCityById(int id)
{
for (auto &c : m_cities) {
if (c.id == id) return &c;
}
return nullptr;
}
City* GameManager::getActiveCityView()
{
return getCityById(m_activeCityViewId);
}
const City* GameManager::getActiveCityView() const
{
for (const auto &c : m_cities) {
if (c.id == m_activeCityViewId) return &c;
}
return nullptr;
}
void GameManager::enterCityView(int cityId)
{
m_activeCityViewId = cityId;
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
void GameManager::exitCityView()
{
m_activeCityViewId = -1;
}
QMap<QString, int> GameManager::getTileYields(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
{
QMap<QString, int> yields;
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
QStringList resources = cCfg.resourcePriority;
if (resources.isEmpty()) {
resources = QStringList{"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"};
}
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
const TileImprovement *imp = getImprovementAt(tx, ty);
const ImprovementConfig *iCfg = imp ? TerrainYieldManager::instance().getImprovementConfig(imp->improvementId) : nullptr;
for (const QString &res : resources) {
int amount = yCfg ? yCfg->getYield(res) : 0;
if (iCfg && iCfg->yieldBonus.contains(res)) {
amount += iCfg->yieldBonus.value(res, 0);
}
yields[res] = amount;
}
return yields;
}
int GameManager::getTileYield(int tx, int ty, const QString &res, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
{
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
int amount = yCfg ? yCfg->getYield(res) : 0;
const TileImprovement *imp = getImprovementAt(tx, ty);
if (imp) {
const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId);
if (iCfg && iCfg->yieldBonus.contains(res)) {
amount += iCfg->yieldBonus.value(res, 0);
}
}
return amount;
}
QString GameManager::getBestTileResource(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
{
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
QStringList priority = cCfg.resourcePriority;
if (priority.isEmpty()) {
priority = QStringList{"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"};
}
QMap<QString, int> yields = getTileYields(tx, ty, mapPoints, numPoints05);
QString bestRes = priority.first();
int maxYield = -1;
for (const QString &res : priority) {
int y = yields.value(res, 0);
if (y > maxYield) {
maxYield = y;
bestRes = res;
}
}
return bestRes;
}
CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
{
CityYieldSummary summary;
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
// Base yields for every city per tick
summary.food += cCfg.getBaseYield("voedsel");
summary.labor += cCfg.getBaseYield("arbeid");
summary.science += cCfg.getBaseYield("kennis");
summary.culture += cCfg.getBaseYield("cultuur");
summary.faith += cCfg.getBaseYield("geloof");
summary.gold += cCfg.getBaseYield("goud");
// Additional yields from worked tiles
for (const auto &pair : city.workedTiles) {
int tx = pair.first.first;
int ty = pair.first.second;
QString assignedRes = pair.second;
int amount = getTileYield(tx, ty, assignedRes, mapPoints, numPoints05);
if (assignedRes == "voedsel") summary.food += amount;
else if (assignedRes == "arbeid") summary.labor += amount;
else if (assignedRes == "kennis") summary.science += amount;
else if (assignedRes == "cultuur") summary.culture += amount;
else if (assignedRes == "geloof") summary.faith += amount;
else if (assignedRes == "goud") summary.gold += amount;
}
// Additional yields and multipliers from completed city buildings
float totalFoodMultiplier = 1.0f;
for (const QString &bId : city.completedBuildings) {
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(bId);
if (!bCfg) continue;
summary.food += bCfg->foodBonus;
summary.labor += bCfg->laborBonus;
summary.science += bCfg->scienceBonus;
summary.culture += bCfg->cultureBonus;
summary.faith += bCfg->faithBonus;
summary.gold += bCfg->goldBonus;
if (bCfg->foodModifier > 0.0f) {
totalFoodMultiplier += bCfg->foodModifier;
}
}
if (totalFoodMultiplier != 1.0f) {
summary.food = (int)std::round(summary.food * totalFoodMultiplier);
}
return summary;
}
void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig();
for (auto &city : m_cities) {
CityYieldSummary yields = calculateCityYields(city, mapPoints, numPoints05);
// 1. Food is converted into inhabitants:
int foodGrown = (int)std::round(yields.food * cfg.foodToInhabitantsRatio);
city.inhabitants += foodGrown;
// 2. Labor contributes to active project or accumulates locally:
if (city.hasActiveProject()) {
city.currentProject.currentLabor += yields.labor;
if (city.currentProject.currentLabor >= city.currentProject.laborCost) {
completeCityProject(city, mapPoints, numPoints05);
}
} else {
city.accumulatedLabor += yields.labor;
}
// 3. National resources go to the owner player:
Player *owner = m_playerManager.getPlayer(city.playerId);
if (owner) {
owner->culture += yields.culture;
owner->science += yields.science;
owner->faith += yields.faith;
owner->gold += yields.gold;
}
// Floating growth text over city
QVector3D ftPos = city.worldPos + QVector3D(0.0f, 0.0f, 1.1f);
QString growthMsg = Localization::instance().get(
"ui.city.growth_float", "+%1 Inhabitants | +%2 Culture")
.arg(foodGrown).arg(yields.culture);
addFloatingText(growthMsg, ftPos, QColor(100, 255, 140), 2.5f);
}
}
bool GameManager::isTileOwned(int tx, int ty, int *outOwnerCityId) const
{
for (const auto &c : m_cities) {
if (c.ownsTile(tx, ty)) {
if (outOwnerCityId) *outOwnerCityId = c.id;
return true;
}
}
return false;
}
bool GameManager::buyTileForCity(int cityId, int tx, int ty)
{
City *city = getCityById(cityId);
if (!city) return false;
// Check if tile is already owned by any city
int existingOwnerId = -1;
if (isTileOwned(tx, ty, &existingOwnerId)) {
return false;
}
const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig();
// Check distance (must be within max workable radius)
int dist = Pathfinder::hexDistance(city->tileX, city->tileY, tx, ty);
if (dist > cfg.maxWorkableRadius) {
return false;
}
// Check culture cost
Player *player = m_playerManager.getPlayer(city->playerId);
if (!player) return false;
int cost = cfg.tileCultureCost;
if (player->culture < cost) {
QVector2D w = Pathfinder::hexToWorld(tx, ty);
QString notEnough = Localization::instance().get("ui.city.not_enough_culture", "Not enough Culture! (%1 needed)").arg(cost);
addFloatingText(notEnough, QVector3D(w.x(), w.y(), 0.8f), QColor(255, 90, 90), 2.0f);
return false;
}
// Deduct culture and claim tile
player->culture -= cost;
city->addTerritory(tx, ty);
QVector2D w = Pathfinder::hexToWorld(tx, ty);
QString boughtMsg = Localization::instance().get("ui.unit.tile_purchased", "Tile purchased! (-%1 Culture)").arg(cost);
addFloatingText(boughtMsg, QVector3D(w.x(), w.y(), 0.8f), QColor(210, 120, 255), 2.5f);
return true;
}
void GameManager::queueCityProject(int cityId, CityProjectType type, const QString &id)
{
City *city = getCityById(cityId);
if (!city) return;
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
int maxQueue = cCfg.maxProductionQueueSize;
if (type == CityProjectType::Unit) {
const UnitConfig *uCfg = m_unitConfigManager.getConfig(id);
if (!uCfg) return;
if (!city->hasActiveProject()) {
city->startProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost);
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
.arg(city->name).arg(uCfg->name);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
} else {
if ((int)city->projectQueue.size() >= maxQueue) {
QString msg = Localization::instance().get("ui.city.queue_full_float", "Production queue is full! (Max %1)").arg(maxQueue);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.0f);
return;
}
city->queueProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost, "center", maxQueue);
int pos = (int)city->projectQueue.size();
QString msg = Localization::instance().get("ui.city.project_queued", "%1: Added %2 to queue (#%3)")
.arg(city->name).arg(uCfg->name).arg(pos);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(100, 200, 255), 2.2f);
}
} else if (type == CityProjectType::Building) {
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(id);
if (!bCfg) return;
if (city->hasBuilding(id)) {
QString msg = Localization::instance().get("ui.city.already_built", "%1 already built in this city!").arg(bCfg->name);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f);
return;
}
if (city->isBuildingQueuedOrBuilt(id)) {
QString msg = Localization::instance().get("ui.city.already_queued", "%1 is already in the production queue!").arg(bCfg->name);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 160, 60), 2.2f);
return;
}
if (!city->hasActiveProject()) {
city->startProject(CityProjectType::Building, id, bCfg->name, bCfg->laborCost, bCfg->district);
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
.arg(city->name).arg(bCfg->name);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
} else {
if ((int)city->projectQueue.size() >= maxQueue) {
QString msg = Localization::instance().get("ui.city.queue_full_float", "Production queue is full! (Max %1)").arg(maxQueue);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.0f);
return;
}
city->queueProject(CityProjectType::Building, id, bCfg->name, bCfg->laborCost, bCfg->district, maxQueue);
int pos = (int)city->projectQueue.size();
QString msg = Localization::instance().get("ui.city.project_queued", "%1: Added %2 to queue (#%3)")
.arg(city->name).arg(bCfg->name).arg(pos);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(100, 200, 255), 2.2f);
}
}
}
void GameManager::setCityProject(int cityId, CityProjectType type, const QString &id)
{
queueCityProject(cityId, type, id);
}
void GameManager::cancelCityProject(int cityId)
{
City *city = getCityById(cityId);
if (!city || !city->hasActiveProject()) return;
QString cancelledName = city->currentProject.name;
city->cancelActiveProject();
QString msg = Localization::instance().get("ui.city.project_cancelled", "%1: Cancelled %2").arg(city->name).arg(cancelledName);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 180, 80), 2.0f);
if (city->hasActiveProject()) {
QString nextMsg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
.arg(city->name).arg(city->currentProject.name);
addFloatingText(nextMsg, city->worldPos + QVector3D(0.0f, 0.0f, 1.4f), QColor(255, 230, 80), 2.5f);
}
}
void GameManager::removeQueuedCityProject(int cityId, int queueIndex)
{
City *city = getCityById(cityId);
if (!city) return;
if (queueIndex >= 0 && queueIndex < (int)city->projectQueue.size()) {
QString itemName = city->projectQueue[queueIndex].name;
city->removeQueuedProject(queueIndex);
QString msg = Localization::instance().get("ui.city.queued_item_removed", "%1: Removed %2 from queue")
.arg(city->name).arg(itemName);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 170, 80), 2.0f);
}
}
void GameManager::completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
if (!city.hasActiveProject()) return;
if (city.currentProject.type == CityProjectType::Unit) {
QString unitId = city.currentProject.id;
const UnitConfig *uCfg = m_unitConfigManager.getConfig(unitId);
// Find spawn location: city center or adjacent empty hex
int spawnX = city.tileX;
int spawnY = city.tileY;
if (getUnitAt(spawnX, spawnY) != nullptr) {
for (int e = 0; e < 6; ++e) {
int nx, ny;
getHexNeighbor(city.tileX, city.tileY, e, nx, ny);
if (nx >= 0 && nx < MAP_WIDTH && ny >= 0 && ny < MAP_HEIGHT) {
if (getUnitAt(nx, ny) == nullptr &&
TerrainMovementManager::instance().isHexPassable(nx, ny, uCfg ? uCfg->movementDomain : MovementDomain::Land, mapPoints, numPoints05, uCfg)) {
spawnX = nx;
spawnY = ny;
break;
}
}
}
}
Unit *spawned = spawnUnit(unitId, city.playerId, spawnX, spawnY, mapPoints, numPoints05);
QString unitName = spawned ? spawned->config.name : (uCfg ? uCfg->name : unitId);
QString msg = Localization::instance().get("ui.city.unit_trained", "%1: %2 trained!")
.arg(city.name).arg(unitName);
addFloatingText(msg, city.worldPos + QVector3D(0.0f, 0.0f, 1.3f), QColor(80, 255, 140), 3.0f);
} else if (city.currentProject.type == CityProjectType::Building) {
QString bId = city.currentProject.id;
city.addBuilding(bId);
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(bId);
QString bName = bCfg ? bCfg->name : bId;
const DistrictConfig *dCfg = m_buildingConfigManager.getDistrict(bCfg ? bCfg->district : "center");
QString dName = dCfg ? dCfg->name : "City Center";
QString msg = Localization::instance().get("ui.city.building_completed", "%1: %2 completed in %3!")
.arg(city.name).arg(bName).arg(dName);
addFloatingText(msg, city.worldPos + QVector3D(0.0f, 0.0f, 1.3f), QColor(80, 240, 255), 3.5f);
}
int excess = city.currentProject.currentLabor - city.currentProject.laborCost;
city.advanceQueue();
if (city.hasActiveProject() && excess > 0) {
city.currentProject.currentLabor += excess;
if (city.currentProject.currentLabor >= city.currentProject.laborCost) {
completeCityProject(city, mapPoints, numPoints05);
}
} else {
city.accumulatedLabor += std::max(0, excess);
}
}
void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
if (!selected) return;
if (targetX == selected->tileX && targetY == selected->tileY) {
return;
}
// Check if target tile is passable for this unit's movement domain
if (!TerrainMovementManager::instance().isHexPassable(
targetX, targetY, selected->config.movementDomain, mapPoints, numPoints05, &selected->config)) {
if (selected->isNaval()) {
addFloatingText(Localization::instance().get("ui.unit.naval_only_water", "Ships can only sail on water!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
} else {
addFloatingText(Localization::instance().get("ui.unit.impassable_terrain", "Impassable terrain!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
}
return;
}
// If unit is busy building, it CANNOT walk right now. Queue the move action!
if (selected->isBuilding()) {
if (m_allowActionQueueing && m_autoQueueMoveWhenBusy) {
if (selected->queueAction(UnitAction(UnitActionType::Move, targetX, targetY))) {
addFloatingText(Localization::instance().get("ui.unit.move_queued", "Movement queued"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
} else {
addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
}
} else {
addFloatingText(Localization::instance().get("ui.unit.busy", "Unit is busy!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
}
return;
}
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05,
selected->config.movementDomain, &selected->config
);
if (path.size() > 1) {
selected->clearActionQueue();
selected->setPath(path);
addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
} else {
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
}
}
void GameManager::commandAttack(Unit *target)
{
Unit *selected = getSelectedUnit();
if (!selected || !target) return;
if (selected->isBuilding()) {
addFloatingText(Localization::instance().get("ui.unit.cannot_act_busy", "Unit cannot perform action while busy!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
if (!selected->canAttack(*target)) {
addFloatingText(Localization::instance().get("ui.unit.out_of_range", "Out of range!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
float damage = (float)selected->config.attackDamage;
selected->performAttack(*target);
QString dmgText = QString("-%1 HP").arg((int)damage);
addFloatingText(dmgText, target->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 50, 50));
if (target->isDead()) {
addFloatingText(Localization::instance().get("ui.unit.defeated", "Defeated!"),
target->worldPos + QVector3D(0, 0, 1.2f), QColor(255, 200, 50), 2.2f);
}
m_actionMode = ActionMode::None;
}
void GameManager::commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
if (!selected || !selected->config.canBuildCity()) return;
if (selected->isBuilding()) {
addFloatingText(Localization::instance().get("ui.unit.cannot_act_busy", "Unit cannot perform action while busy!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
// If unit is moving, queue city founding upon arrival
if (selected->state == UnitState::Moving) {
if (selected->hasQueuedActionType(UnitActionType::BuildCity)) {
selected->removeQueuedActionsOfType(UnitActionType::BuildCity);
addFloatingText(Localization::instance().get("ui.unit.action_cancelled", "%1 cancelled").arg("Found City"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80));
return;
}
if (selected->queueAction(UnitAction(UnitActionType::BuildCity, selected->targetTileX, selected->targetTileY))) {
addFloatingText(Localization::instance().get("ui.unit.city_queued", "Found City 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;
// Check if tile already has a city
for (const auto &c : m_cities) {
if (c.tileX == tx && c.tileY == ty) {
addFloatingText(Localization::instance().get("ui.unit.city_already_here", "A city already exists here!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
}
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
int cId = getNextCityId();
QString cityName = QString("City %1").arg(cId);
QVector2D w = Pathfinder::hexToWorld(tx, ty);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
City newCity(cId, selected->playerId, cityName, tx, ty,
QVector3D(w.x(), w.y(), gz),
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
// Assign initial worker to center tile with best yield resource
QString defRes = getBestTileResource(tx, ty, mapPoints, numPoints05);
newCity.assignWorker(tx, ty, defRes);
m_cities.push_back(newCity);
addFloatingText(Localization::instance().get("ui.unit.city_founded", "%1 founded!").arg(cityName),
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f);
// Consuming settler
selected->takeDamage(99999.0f);
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
void GameManager::addFloatingText(const QString &text, const QVector3D &worldPos, const QColor &color, float duration)
{
m_floatingTexts.push_back({text, worldPos, duration, duration, color});
}
const TileImprovement* GameManager::getImprovementAt(int tx, int ty) const
{
auto it = m_tileImprovements.find({tx, ty});
if (it != m_tileImprovements.end()) {
return &it->second;
}
return nullptr;
}
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();
if (!selected || !selected->config.canBuildFarm()) 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;
}
if (!selected->config.infiniteCharges && selected->buildCharges <= 0) {
addFloatingText(Localization::instance().get("ui.unit.no_build_charges", "No build charges remaining!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
// If unit is currently moving: toggle or queue build action on arrival!
if (selected->state == UnitState::Moving) {
if (selected->hasQueuedActionType(UnitActionType::BuildImprovement)) {
selected->removeQueuedActionsOfType(UnitActionType::BuildImprovement);
addFloatingText(Localization::instance().get("ui.unit.action_cancelled", "%1 cancelled").arg("Build Farm"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80));
return;
}
int destX = selected->targetTileX;
int destY = selected->targetTileY;
// Check if destination tile already has an improvement
if (hasImprovementAt(destX, destY)) {
addFloatingText(Localization::instance().get("ui.unit.tile_occupied", "Tile already has an improvement!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
// Check destination terrain type against allowed terrains
QString tType = TerrainYieldManager::instance().getTerrainType(destX, destY, mapPoints, numPoints05);
const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig("farm");
QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"};
if (!validTerrains.contains(tType)) {
QString validStr = validTerrains.join(", ");
addFloatingText(Localization::instance().get("ui.unit.cannot_build_destination", "Cannot build on destination terrain! Allowed on: %1").arg(validStr),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
if (selected->queueAction(UnitAction(UnitActionType::BuildImprovement, destX, destY, "farm"))) {
addFloatingText(Localization::instance().get("ui.unit.farm_queued", "Build Farm 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;
// Check if tile already has an improvement
if (hasImprovementAt(tx, ty)) {
addFloatingText(Localization::instance().get("ui.unit.tile_occupied", "Tile already has an improvement!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
// Check terrain type against allowed terrains loaded from improvements.yaml
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig("farm");
QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"};
if (!validTerrains.contains(tType)) {
QString validStr = validTerrains.join(", ");
addFloatingText(Localization::instance().get("ui.unit.cannot_build_here", "Cannot build here! Allowed on: %1").arg(validStr),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
// Start building process (duration comes from the builder unit's action speed)
float duration = selected->config.getActionDuration("boerderij_bouwen", 10.0f);
selected->startBuilding("farm", duration);
addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm..."),
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!
TileImprovement imp;
imp.improvementId = impId;
imp.tileX = tx;
imp.tileY = ty;
imp.playerId = unit.playerId;
imp.worldPos = QVector3D(w.x(), w.y(), gz);
m_tileImprovements[{tx, ty}] = imp;
unit.cancelBuilding();
if (!unit.config.infiniteCharges && unit.buildCharges > 0) {
unit.buildCharges -= 1;
if (unit.buildCharges <= 0) {
addFloatingText(Localization::instance().get("ui.unit.farm_completed_consumed", "Farm completed! (Builder consumed)"),
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
unit.takeDamage(99999.0f);
if (unit.id == m_selectedUnitId) {
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
return;
} else {
QString chargeWord = unit.buildCharges == 1
? Localization::instance().get("ui.unit.charge_singular", "charge")
: Localization::instance().get("ui.unit.charge_plural", "charges");
QString msg = Localization::instance().get("ui.unit.farm_completed_charges", "Farm completed! (%1 %2 remaining)")
.arg(unit.buildCharges)
.arg(chargeWord);
addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
return;
}
}
addFloatingText(Localization::instance().get("ui.unit.farm_completed", "Farm completed!"),
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
}
void GameManager::processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
if (unit.isDead() || unit.state != UnitState::Idle || !unit.hasQueuedAction()) {
return;
}
UnitAction action = unit.popNextQueuedAction();
switch (action.type) {
case UnitActionType::Move: {
int tx = action.targetX;
int ty = action.targetY;
if (tx == unit.tileX && ty == unit.tileY) {
return;
}
if (!TerrainMovementManager::instance().isHexPassable(
tx, ty, unit.config.movementDomain, mapPoints, numPoints05, &unit.config)) {
addFloatingText(Localization::instance().get("ui.unit.impassable_terrain", "Impassable terrain!"),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
return;
}
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
unit.tileX, unit.tileY, tx, ty, mapPoints, numPoints05,
unit.config.movementDomain, &unit.config
);
if (path.size() > 1) {
unit.setPath(path);
addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
} else {
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
}
break;
}
case UnitActionType::BuildImprovement: {
QString impId = action.extraData.isEmpty() ? "farm" : action.extraData;
int tx = unit.tileX;
int ty = unit.tileY;
if (!unit.config.canBuildFarm()) {
return;
}
if (!unit.config.infiniteCharges && unit.buildCharges <= 0) {
addFloatingText(Localization::instance().get("ui.unit.no_build_charges", "No build charges remaining!"),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
if (hasImprovementAt(tx, ty)) {
addFloatingText(Localization::instance().get("ui.unit.tile_occupied", "Tile already has an improvement!"),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig(impId);
QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"};
if (!validTerrains.contains(tType)) {
QString validStr = validTerrains.join(", ");
addFloatingText(Localization::instance().get("ui.unit.cannot_build_here", "Cannot build here! Allowed on: %1").arg(validStr),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
float duration = unit.config.getActionDuration("boerderij_bouwen", 10.0f);
unit.startBuilding(impId, duration);
addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm..."),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
break;
}
case UnitActionType::BuildCity: {
if (!unit.config.canBuildCity()) {
return;
}
int tx = unit.tileX;
int ty = unit.tileY;
// Check if tile already has a city
for (const auto &c : m_cities) {
if (c.tileX == tx && c.tileY == ty) {
addFloatingText(Localization::instance().get("ui.unit.city_already_here", "A city already exists here!"),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
return;
}
}
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
int cId = getNextCityId();
QString cityName = QString("City %1").arg(cId);
QVector2D w = Pathfinder::hexToWorld(tx, ty);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
City newCity(cId, unit.playerId, cityName, tx, ty,
QVector3D(w.x(), w.y(), gz),
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
QString defRes = getBestTileResource(tx, ty, mapPoints, numPoints05);
newCity.assignWorker(tx, ty, defRes);
m_cities.push_back(newCity);
addFloatingText(Localization::instance().get("ui.unit.city_founded", "%1 founded!").arg(cityName),
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f);
unit.takeDamage(99999.0f);
if (unit.id == m_selectedUnitId) {
m_selectedUnitId = -1;
m_actionMode = ActionMode::None;
}
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) {
if (other->id == action.targetUnitId && !other->isDead()) {
target = other.get();
break;
}
}
if (target && unit.canAttack(*target)) {
unit.performAttack(*target);
addFloatingText(QString("-%1 HP").arg(unit.config.attackDamage),
target->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 50, 50));
if (target->isDead()) {
addFloatingText(Localization::instance().get("ui.unit.defeated", "Defeated!"),
target->worldPos + QVector3D(0, 0, 1.2f), QColor(255, 200, 50), 2.2f);
}
}
break;
}
default:
break;
}
}