#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_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); } // Load unit YAML configurations QString unitsConfigDir = baseDir + "/config/units"; m_unitConfigManager.loadFromDirectory(unitsConfigDir); 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 QString tType = TerrainYieldManager::instance().getTerrainType(35, 17, mapPoints, numPoints05); const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType); QString defRes = yCfg ? yCfg->primaryResource : "voedsel"; 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); 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); } } } // 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; } 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; QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType); if (!yCfg) continue; int amount = yCfg->getYield(assignedRes); // Add bonus from tile improvements (e.g. farm bonus) const TileImprovement *imp = getImprovementAt(tx, ty); if (imp) { const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId); if (iCfg && iCfg->yieldBonus.contains(assignedRes)) { amount += iCfg->yieldBonus.value(assignedRes, 0); } } 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; } 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 is accumulated locally in the city: 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); addFloatingText(QString("+%1 Inwoners | +%2 Cultuur").arg(foodGrown).arg(yields.culture), 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); addFloatingText(QString("Niet genoeg Cultuur! (%1 nodig)").arg(cost), 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); addFloatingText(QString("Tegel gekocht! (-%1 Cultuur)").arg(cost), QVector3D(w.x(), w.y(), 0.8f), QColor(210, 120, 255), 2.5f); return true; } 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; } std::vector> path = Pathfinder::findPath( selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05, selected->config.movementDomain, &selected->config ); if (path.size() > 1) { 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->canAttack(*target)) { addFloatingText("Buiten bereik!", 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("Verslagen!", 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; // Check if tile already has a city for (const auto &c : m_cities) { if (c.tileX == selected->tileX && c.tileY == selected->tileY) { addFloatingText("Al een stad hier!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); return; } } const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); int cId = getNextCityId(); QString cityName = QString("Stad %1").arg(cId); QVector2D w = Pathfinder::hexToWorld(selected->tileX, selected->tileY); float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05); City newCity(cId, selected->playerId, cityName, selected->tileX, selected->tileY, QVector3D(w.x(), w.y(), gz), cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius); // Assign initial worker to center tile QString tType = TerrainYieldManager::instance().getTerrainType(selected->tileX, selected->tileY, mapPoints, numPoints05); const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType); QString defRes = yCfg ? yCfg->primaryResource : "voedsel"; newCity.assignWorker(selected->tileX, selected->tileY, defRes); m_cities.push_back(newCity); addFloatingText(QString("%1 gesticht!").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(); } void GameManager::commandBuildFarmSelected(Matrix2D *mapPoints, int numPoints05) { Unit *selected = getSelectedUnit(); if (!selected || !selected->config.canBuildFarm()) return; if (selected->isBuilding()) { addFloatingText("Al bezig met bouwen!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 200, 70)); return; } int tx = selected->tileX; int ty = selected->tileY; // Check if tile already has an improvement if (hasImprovementAt(tx, ty)) { addFloatingText("Er staat hier al een boerderij!", 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)) { addFloatingText("Kan hier niet bouwen! Alleen op gras en toendra.", 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("Boerderij in aanbouw...", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.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; if (hasImprovementAt(tx, ty)) { unit.cancelBuilding(); return; } // Place improvement now that it is completed! QVector2D w = Pathfinder::hexToWorld(tx, ty); float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05); TileImprovement imp; imp.improvementId = impId; imp.tileX = tx; 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("Boerderij voltooid! (Builder verbruikt)", 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 msg = QString("Boerderij voltooid! (%1 %2 over)") .arg(unit.buildCharges) .arg(unit.buildCharges == 1 ? "lading" : "ladingen"); addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f); return; } } addFloatingText("Boerderij voltooid!", QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f); }