#include "gamemanager.h" #include "pathfinder.h" #include "yamlparser.h" #include "terrainyieldmanager.h" #include "terrainmovementmanager.h" #include "localization.h" #include #include 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 *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); // Load action needed queue configuration QString actionNeededConfigPath = baseDir + "/config/action_needed.yaml"; m_actionNeededManager.loadConfigFile(actionNeededConfigPath); 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 *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(id, playerId, *cfg, tileX, tileY, gz); unit->maxQueueSize = m_maxActionQueueSize; m_units.push_back(unit); return unit.get(); } void GameManager::update(float deltaTime, Matrix2D *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) { if (u->hasRoadRoute()) { advanceRoadRoute(*u, mapPoints, numPoints05); } else if (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; } } // Update Action Needed Queue for active human player 1 m_actionNeededManager.update(deltaTime, 1, m_units, m_cities); } 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 GameManager::getTileYields(int tx, int ty, Matrix2D *mapPoints, int numPoints05) const { QMap 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 *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 *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 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 *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 *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 *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 *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> path = Pathfinder::findPath( selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05, selected->config.movementDomain, &selected->config ); if (path.size() > 1) { selected->clearActionQueue(); selected->clearRoadRoute(); 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 *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 *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 *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 *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::commandBuildRoadToTileSelected(int targetX, int targetY, Matrix2D *mapPoints, int numPoints05) { Unit *selected = getSelectedUnit(); if (!selected || !selected->config.canBuildRoad()) return; if (!TerrainMovementManager::instance().isHexPassable( targetX, targetY, 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; } // Pathfind from current tile to target tile std::vector> route = Pathfinder::findPath( selected->tileX, selected->tileY, targetX, targetY, MovementDomain::Land, mapPoints, numPoints05, &selected->config ); if (route.empty()) { addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"), selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90)); return; } selected->clearActionQueue(); selected->startRoadRoute(route); QString defaultType = m_roadConfigManager.defaultRoadType(); const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType); QString roadName = rCfg ? rCfg->name : "Road"; addFloatingText(Localization::instance().get("ui.unit.road_route_started", "Building road route to (%1, %2)...").arg(targetX).arg(targetY), selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.2f); advanceRoadRoute(*selected, mapPoints, numPoints05); } void GameManager::advanceRoadRoute(Unit &unit, Matrix2D *mapPoints, int numPoints05) { if (!unit.hasRoadRoute()) return; while (unit.roadRouteIndex < unit.roadRoute.size()) { auto nextHex = unit.roadRoute[unit.roadRouteIndex]; int tx = nextHex.first; int ty = nextHex.second; // If unit is not on this tile yet, move to it! if (unit.tileX != tx || unit.tileY != ty) { unit.setPath({ {unit.tileX, unit.tileY}, {tx, ty} }); unit.state = UnitState::Moving; return; } // Unit is on this tile! Does it already have a road? if (hasRoadAt(tx, ty)) { // Already has a road, skip to next tile along route unit.advanceRoadRouteStep(); continue; } // Start building road on this tile QString defaultType = m_roadConfigManager.defaultRoadType(); const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType); QString roadName = rCfg ? rCfg->name : "Road"; int ticks = m_roadConfigManager.buildDurationTicks(); float duration = (float)ticks * m_movementTickDuration; unit.startBuilding(QString("road:%1").arg(defaultType), 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); return; } // Finished entire route! unit.clearRoadRoute(); QVector2D w = Pathfinder::hexToWorld(unit.tileX, unit.tileY); float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05); addFloatingText(Localization::instance().get("ui.unit.road_route_completed", "Road route completed!"), QVector3D(w.x(), w.y(), gz + 1.2f), QColor(100, 255, 140), 3.0f); } void GameManager::completeUnitBuilding(Unit &unit, Matrix2D *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); if (unit.hasRoadRoute()) { unit.advanceRoadRouteStep(); advanceRoadRoute(unit, mapPoints, numPoints05); } 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 *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> 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; } }