1902 lines
76 KiB
C++
1902 lines
76 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_isTechTreeOpen(false), m_actionMode(ActionMode::None),
|
|
m_movementTickDuration(30.0f), m_tickTimer(0.0f),
|
|
m_simSpeedMultiplier(1.0f),
|
|
m_maxActionQueueSize(15), 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(15);
|
|
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 technology tree YAML configuration
|
|
QString techConfigPath = baseDir + "/config/technologies.yaml";
|
|
m_techTreeManager.loadConfigFile(techConfigPath);
|
|
|
|
// 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
|
|
// Note: science is a per-tick generation rate and is not stockpiled
|
|
for (auto &player : m_playerManager.getPlayersMutable()) {
|
|
player.culture = cCfg.initialNationalResources.value("cultuur", 10000);
|
|
player.science = 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);
|
|
|
|
// Update initial per-tick science generation rate for all players
|
|
for (auto &player : m_playerManager.getPlayersMutable()) {
|
|
player.science = calculatePlayerSciencePerTick(player.id, mapPoints, numPoints05);
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
// Keep player science rate synchronized with current city yields (science is generated per tick, not saved)
|
|
for (auto &player : m_playerManager.getPlayersMutable()) {
|
|
player.science = calculatePlayerSciencePerTick(player.id, mapPoints, 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->currentAction.isValid()) {
|
|
continueCurrentUnitAction(*u, mapPoints, numPoints05);
|
|
}
|
|
if (!u->isDead() && u->state == UnitState::Idle && !u->currentAction.isValid() && 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;
|
|
}
|
|
}
|
|
|
|
// Check if Player 1 needs to select a technology to research
|
|
const Player *p1 = m_playerManager.getPlayer(1);
|
|
bool p1HasCities = false;
|
|
for (const auto &c : m_cities) {
|
|
if (c.playerId == 1) {
|
|
p1HasCities = true;
|
|
break;
|
|
}
|
|
}
|
|
bool hasUnresearchedTechs = p1 && ((int)p1->researchedTechs.size() < m_techTreeManager.getAllTechs().size());
|
|
bool needsResearch = p1 && p1HasCities && !p1->hasActiveResearch() && hasUnresearchedTechs;
|
|
|
|
// Update Action Needed Queue for active human player 1
|
|
m_actionNeededManager.update(deltaTime, 1, m_units, m_cities, needsResearch);
|
|
}
|
|
|
|
void GameManager::openTechTree()
|
|
{
|
|
m_isTechTreeOpen = true;
|
|
m_actionMode = ActionMode::None;
|
|
}
|
|
|
|
void GameManager::closeTechTree()
|
|
{
|
|
m_isTechTreeOpen = false;
|
|
}
|
|
|
|
void GameManager::toggleTechTree()
|
|
{
|
|
if (m_isTechTreeOpen) {
|
|
closeTechTree();
|
|
} else {
|
|
openTechTree();
|
|
}
|
|
}
|
|
|
|
int GameManager::calculatePlayerSciencePerTick(int playerId, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
|
|
{
|
|
int totalScience = 0;
|
|
for (const auto &city : m_cities) {
|
|
if (city.playerId == playerId) {
|
|
CityYieldSummary yields = calculateCityYields(city, mapPoints, numPoints05);
|
|
totalScience += yields.science;
|
|
}
|
|
}
|
|
return totalScience;
|
|
}
|
|
|
|
int GameManager::getTechRemainingScience(const Player &player, const QString &techId) const
|
|
{
|
|
const TechConfig *cfg = m_techTreeManager.getTech(techId);
|
|
if (!cfg) return 0;
|
|
if (player.hasResearched(techId)) return 0;
|
|
int progress = player.getTechProgress(techId);
|
|
return std::max(0, cfg->scienceCost - progress);
|
|
}
|
|
|
|
bool GameManager::selectTechForPlayer(int playerId, const QString &targetTechId, bool appendToQueue)
|
|
{
|
|
Player *player = m_playerManager.getPlayer(playerId);
|
|
if (!player) return false;
|
|
|
|
const TechConfig *targetCfg = m_techTreeManager.getTech(targetTechId);
|
|
if (!targetCfg) return false;
|
|
|
|
if (player->hasResearched(targetTechId)) {
|
|
return false;
|
|
}
|
|
|
|
// Determine which techs are already satisfied (researched, or already queued if appending)
|
|
std::set<QString> satisfied = player->researchedTechs;
|
|
if (appendToQueue) {
|
|
for (const QString &qId : player->researchQueue) {
|
|
satisfied.insert(qId);
|
|
}
|
|
if (satisfied.find(targetTechId) != satisfied.end()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Collect all unresearched (and unqueued if appending) prerequisites of targetTechId + targetTechId itself
|
|
std::set<QString> requiredSet;
|
|
std::vector<QString> stack;
|
|
stack.push_back(targetTechId);
|
|
|
|
while (!stack.empty()) {
|
|
QString cur = stack.back();
|
|
stack.pop_back();
|
|
|
|
if (satisfied.find(cur) != satisfied.end() || requiredSet.find(cur) != requiredSet.end()) {
|
|
continue;
|
|
}
|
|
const TechConfig *cCfg = m_techTreeManager.getTech(cur);
|
|
if (!cCfg) continue;
|
|
|
|
requiredSet.insert(cur);
|
|
for (const QString &pre : cCfg->prerequisites) {
|
|
if (satisfied.find(pre) == satisfied.end() && requiredSet.find(pre) == requiredSet.end()) {
|
|
stack.push_back(pre);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build ordered queue:
|
|
// At each step, among all technologies in requiredSet whose prerequisites are all satisfied,
|
|
// choose the technology with the smallest remaining science work (scienceCost - currentProgress).
|
|
// Note: targetTechId itself only becomes available once all its prerequisites are placed in the queue.
|
|
std::vector<QString> orderedQueue;
|
|
orderedQueue.reserve(requiredSet.size());
|
|
std::set<QString> currentSatisfied = satisfied;
|
|
std::set<QString> remainingToSchedule = requiredSet;
|
|
|
|
while (!remainingToSchedule.empty()) {
|
|
std::vector<QString> readyTechs;
|
|
for (const QString &candId : remainingToSchedule) {
|
|
const TechConfig *candCfg = m_techTreeManager.getTech(candId);
|
|
if (!candCfg) continue;
|
|
|
|
bool allPrereqsMet = true;
|
|
for (const QString &pre : candCfg->prerequisites) {
|
|
if (currentSatisfied.find(pre) == currentSatisfied.end()) {
|
|
allPrereqsMet = false;
|
|
break;
|
|
}
|
|
}
|
|
if (allPrereqsMet) {
|
|
readyTechs.push_back(candId);
|
|
}
|
|
}
|
|
|
|
if (readyTechs.empty()) {
|
|
// Fallback in case of malformed cyclic config in YAML
|
|
for (const QString &candId : remainingToSchedule) {
|
|
readyTechs.push_back(candId);
|
|
}
|
|
}
|
|
|
|
// Sort ready techs by smallest remaining science work (already partially researched techs have less work left)
|
|
std::sort(readyTechs.begin(), readyTechs.end(), [&](const QString &a, const QString &b) {
|
|
// Ensure targetTechId comes after any other ready tech if appending or tie-breaking
|
|
if (a == targetTechId && b != targetTechId) return false;
|
|
if (b == targetTechId && a != targetTechId) return true;
|
|
|
|
int remA = getTechRemainingScience(*player, a);
|
|
int remB = getTechRemainingScience(*player, b);
|
|
if (remA != remB) return remA < remB;
|
|
|
|
const TechConfig *ta = m_techTreeManager.getTech(a);
|
|
const TechConfig *tb = m_techTreeManager.getTech(b);
|
|
if (ta && tb) {
|
|
if (ta->column != tb->column) return ta->column < tb->column;
|
|
if (ta->row != tb->row) return ta->row < tb->row;
|
|
}
|
|
return a < b;
|
|
});
|
|
|
|
QString chosen = readyTechs.front();
|
|
orderedQueue.push_back(chosen);
|
|
currentSatisfied.insert(chosen);
|
|
remainingToSchedule.erase(chosen);
|
|
}
|
|
|
|
if (appendToQueue) {
|
|
player->researchQueue.insert(player->researchQueue.end(), orderedQueue.begin(), orderedQueue.end());
|
|
} else {
|
|
player->researchQueue = orderedQueue;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void GameManager::removeTechFromPlayerQueue(int playerId, const QString &techId)
|
|
{
|
|
Player *player = m_playerManager.getPlayer(playerId);
|
|
if (!player) return;
|
|
|
|
// Remove techId and any queued tech that transitively depends on techId
|
|
std::set<QString> removed;
|
|
removed.insert(techId);
|
|
|
|
bool changed = true;
|
|
while (changed) {
|
|
changed = false;
|
|
for (const QString &qId : player->researchQueue) {
|
|
if (removed.find(qId) != removed.end()) continue;
|
|
const TechConfig *cfg = m_techTreeManager.getTech(qId);
|
|
if (!cfg) continue;
|
|
for (const QString &pre : cfg->prerequisites) {
|
|
if (removed.find(pre) != removed.end()) {
|
|
removed.insert(qId);
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
player->researchQueue.erase(
|
|
std::remove_if(player->researchQueue.begin(), player->researchQueue.end(),
|
|
[&](const QString &id) { return removed.find(id) != removed.end(); }),
|
|
player->researchQueue.end()
|
|
);
|
|
}
|
|
|
|
void GameManager::clearPlayerResearchQueue(int playerId)
|
|
{
|
|
Player *player = m_playerManager.getPlayer(playerId);
|
|
if (!player) return;
|
|
player->researchQueue.clear();
|
|
}
|
|
|
|
void GameManager::advancePlayerResearch(Player &player, int scienceGenerated)
|
|
{
|
|
if (scienceGenerated <= 0 || player.researchQueue.empty()) {
|
|
// Science cannot be saved when no technology is being researched
|
|
return;
|
|
}
|
|
|
|
int remainingScienceToApply = scienceGenerated;
|
|
|
|
while (remainingScienceToApply > 0 && !player.researchQueue.empty()) {
|
|
QString activeId = player.researchQueue.front();
|
|
const TechConfig *cfg = m_techTreeManager.getTech(activeId);
|
|
if (!cfg) {
|
|
player.researchQueue.erase(player.researchQueue.begin());
|
|
continue;
|
|
}
|
|
|
|
// Verify prerequisites are met before applying science
|
|
if (!player.arePrerequisitesMet(cfg->prerequisites)) {
|
|
// Re-run prerequisite ordering for the target tech
|
|
QString targetId = player.researchQueue.back();
|
|
selectTechForPlayer(player.id, targetId, false);
|
|
if (player.researchQueue.empty()) break;
|
|
activeId = player.researchQueue.front();
|
|
cfg = m_techTreeManager.getTech(activeId);
|
|
if (!cfg) break;
|
|
}
|
|
|
|
int curProg = player.getTechProgress(activeId);
|
|
int needed = std::max(0, cfg->scienceCost - curProg);
|
|
|
|
if (remainingScienceToApply >= needed) {
|
|
// Technology completed!
|
|
remainingScienceToApply -= needed;
|
|
player.techProgress[activeId] = cfg->scienceCost;
|
|
player.researchedTechs.insert(activeId);
|
|
player.researchQueue.erase(player.researchQueue.begin());
|
|
|
|
// Remove any newly obsolete unbuilt projects from this player's city queues
|
|
// (Already built buildings and existing units on the map do NOT disappear!)
|
|
for (auto &city : m_cities) {
|
|
if (city.playerId != player.id) continue;
|
|
|
|
// Remove obsolete items from waiting projectQueue
|
|
city.projectQueue.erase(
|
|
std::remove_if(city.projectQueue.begin(), city.projectQueue.end(), [&](const CityProject &proj) {
|
|
if (proj.type == CityProjectType::Unit) {
|
|
const UnitConfig *uCfg = m_unitConfigManager.getConfig(proj.id);
|
|
return uCfg && player.isItemObsolete(uCfg->obsoleteTech);
|
|
} else if (proj.type == CityProjectType::Building) {
|
|
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(proj.id);
|
|
return bCfg && player.isItemObsolete(bCfg->obsoleteTech);
|
|
}
|
|
return false;
|
|
}),
|
|
city.projectQueue.end()
|
|
);
|
|
|
|
// Check if currentProject became obsolete
|
|
if (city.hasActiveProject()) {
|
|
bool activeObsolete = false;
|
|
if (city.currentProject.type == CityProjectType::Unit) {
|
|
const UnitConfig *uCfg = m_unitConfigManager.getConfig(city.currentProject.id);
|
|
activeObsolete = (uCfg && player.isItemObsolete(uCfg->obsoleteTech));
|
|
} else if (city.currentProject.type == CityProjectType::Building) {
|
|
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(city.currentProject.id);
|
|
activeObsolete = (bCfg && player.isItemObsolete(bCfg->obsoleteTech));
|
|
}
|
|
if (activeObsolete) {
|
|
city.cancelActiveProject();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show floating notification over player's capital / first city
|
|
for (const auto &city : m_cities) {
|
|
if (city.playerId == player.id) {
|
|
QString msg = Localization::instance().get("ui.tech_tree.tech_completed_float", "Technology Researched: %1!")
|
|
.arg(cfg->name);
|
|
addFloatingText(msg, city.worldPos + QVector3D(0.0f, 0.0f, 1.5f), QColor(60, 220, 255), 3.5f);
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
player.techProgress[activeId] = curProg + remainingScienceToApply;
|
|
remainingScienceToApply = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
QStringList GameManager::getTechUnlocks(const QString &techId) const
|
|
{
|
|
QStringList unlocks;
|
|
if (techId.isEmpty()) return unlocks;
|
|
|
|
const auto &buildings = m_buildingConfigManager.getAllBuildings();
|
|
for (auto it = buildings.begin(); it != buildings.end(); ++it) {
|
|
if (it.value().requiredTech == techId) {
|
|
unlocks.append(it.value().name);
|
|
}
|
|
}
|
|
|
|
const auto &units = m_unitConfigManager.getAllConfigs();
|
|
for (auto it = units.begin(); it != units.end(); ++it) {
|
|
if (it.value().requiredTech == techId) {
|
|
unlocks.append(it.value().name);
|
|
}
|
|
}
|
|
return unlocks;
|
|
}
|
|
|
|
QStringList GameManager::getTechObsoletes(const QString &techId) const
|
|
{
|
|
QStringList obsoletes;
|
|
if (techId.isEmpty()) return obsoletes;
|
|
|
|
const auto &buildings = m_buildingConfigManager.getAllBuildings();
|
|
for (auto it = buildings.begin(); it != buildings.end(); ++it) {
|
|
if (it.value().obsoleteTech == techId) {
|
|
obsoletes.append(it.value().name);
|
|
}
|
|
}
|
|
|
|
const auto &units = m_unitConfigManager.getAllConfigs();
|
|
for (auto it = units.begin(); it != units.end(); ++it) {
|
|
if (it.value().obsoleteTech == techId) {
|
|
obsoletes.append(it.value().name);
|
|
}
|
|
}
|
|
return obsoletes;
|
|
}
|
|
|
|
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_isTechTreeOpen = false;
|
|
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();
|
|
QMap<int, int> playerTickScience;
|
|
|
|
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 (science is applied to research, not stockpiled):
|
|
Player *owner = m_playerManager.getPlayer(city.playerId);
|
|
if (owner) {
|
|
owner->culture += yields.culture;
|
|
owner->faith += yields.faith;
|
|
owner->gold += yields.gold;
|
|
playerTickScience[city.playerId] += yields.science;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 4. Apply generated science per tick directly to each player's active research queue
|
|
for (auto &player : m_playerManager.getPlayersMutable()) {
|
|
int generatedSci = playerTickScience.value(player.id, 0);
|
|
player.science = generatedSci;
|
|
advancePlayerResearch(player, generatedSci);
|
|
}
|
|
}
|
|
|
|
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 Player *player = m_playerManager.getPlayer(city->playerId);
|
|
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 (player) {
|
|
if (!player->isItemUnlocked(uCfg->requiredTech)) {
|
|
const TechConfig *tCfg = m_techTreeManager.getTech(uCfg->requiredTech);
|
|
QString tName = tCfg ? tCfg->name : uCfg->requiredTech;
|
|
QString msg = Localization::instance().get("ui.city.requires_tech", "%1 requires technology: %2!")
|
|
.arg(uCfg->name).arg(tName);
|
|
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f);
|
|
return;
|
|
}
|
|
if (player->isItemObsolete(uCfg->obsoleteTech)) {
|
|
QString msg = Localization::instance().get("ui.city.obsolete_item", "%1 is obsolete and can no longer be built!")
|
|
.arg(uCfg->name);
|
|
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f);
|
|
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 (player) {
|
|
if (!player->isItemUnlocked(bCfg->requiredTech)) {
|
|
const TechConfig *tCfg = m_techTreeManager.getTech(bCfg->requiredTech);
|
|
QString tName = tCfg ? tCfg->name : bCfg->requiredTech;
|
|
QString msg = Localization::instance().get("ui.city.requires_tech", "%1 requires technology: %2!")
|
|
.arg(bCfg->name).arg(tName);
|
|
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f);
|
|
return;
|
|
}
|
|
if (player->isItemObsolete(bCfg->obsoleteTech)) {
|
|
QString msg = Localization::instance().get("ui.city.obsolete_item", "%1 is obsolete and can no longer be built!")
|
|
.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::getPlannedOriginTile(const Unit &unit, bool appendWithShift, int &outX, int &outY) const
|
|
{
|
|
if (appendWithShift && !unit.actionQueue.empty()) {
|
|
outX = unit.actionQueue.back().targetX;
|
|
outY = unit.actionQueue.back().targetY;
|
|
return;
|
|
}
|
|
if (unit.currentAction.isValid() && unit.currentAction.targetX >= 0 && unit.currentAction.targetY >= 0) {
|
|
outX = unit.currentAction.targetX;
|
|
outY = unit.currentAction.targetY;
|
|
return;
|
|
}
|
|
if (unit.hasRoadRoute()) {
|
|
outX = unit.roadRoute.back().first;
|
|
outY = unit.roadRoute.back().second;
|
|
return;
|
|
}
|
|
if (unit.state == UnitState::Moving && !unit.path.empty()) {
|
|
outX = unit.path.back().first;
|
|
outY = unit.path.back().second;
|
|
return;
|
|
}
|
|
outX = unit.tileX;
|
|
outY = unit.tileY;
|
|
}
|
|
|
|
bool GameManager::scheduleOrStartUnitAction(Unit &unit, const UnitAction &action, bool appendWithShift,
|
|
Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
|
{
|
|
if (!appendWithShift) {
|
|
// Without Shift: cancel everything queued after the current action
|
|
// (the current action itself is executed completely, and the new action is scheduled right after it)
|
|
unit.clearActionQueue();
|
|
}
|
|
|
|
if (!unit.hasActiveAction()) {
|
|
return startUnitAction(unit, action, mapPoints, numPoints05);
|
|
}
|
|
|
|
if (!m_allowActionQueueing) {
|
|
addFloatingText(Localization::instance().get("ui.unit.busy", "Unit is busy!"),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
|
|
return false;
|
|
}
|
|
|
|
if (!unit.queueAction(action)) {
|
|
addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
|
|
return false;
|
|
}
|
|
|
|
QString summary = unit.getActionSummary(action);
|
|
QVector2D w = Pathfinder::hexToWorld(action.targetX, action.targetY);
|
|
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
|
|
if (appendWithShift) {
|
|
int pos = (int)unit.actionQueue.size();
|
|
QString msg = Localization::instance().get("ui.unit.action_queued_count", "%1 added to queue (#%2)")
|
|
.arg(summary).arg(pos);
|
|
addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.9f), QColor(100, 220, 255), 2.2f);
|
|
} else {
|
|
QString msg = Localization::instance().get("ui.unit.action_scheduled_after_current", "%1 scheduled after current action")
|
|
.arg(summary);
|
|
addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.9f), QColor(255, 220, 90), 2.2f);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GameManager::startUnitAction(Unit &unit, const UnitAction &action,
|
|
Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
|
{
|
|
if (unit.isDead() || !action.isValid()) {
|
|
return false;
|
|
}
|
|
|
|
unit.currentAction = action;
|
|
|
|
switch (action.type) {
|
|
case UnitActionType::Move: {
|
|
int tx = action.targetX;
|
|
int ty = action.targetY;
|
|
if (tx == unit.tileX && ty == unit.tileY) {
|
|
unit.currentAction = UnitAction();
|
|
return false;
|
|
}
|
|
if (!TerrainMovementManager::instance().isHexPassable(
|
|
tx, ty, unit.config.movementDomain, mapPoints, numPoints05, &unit.config)) {
|
|
if (unit.isNaval()) {
|
|
addFloatingText(Localization::instance().get("ui.unit.naval_only_water", "Ships can only sail on water!"),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
|
} else {
|
|
addFloatingText(Localization::instance().get("ui.unit.impassable_terrain", "Impassable terrain!"),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
|
}
|
|
unit.currentAction = UnitAction();
|
|
return false;
|
|
}
|
|
|
|
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));
|
|
return true;
|
|
} 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);
|
|
unit.currentAction = UnitAction();
|
|
return false;
|
|
}
|
|
}
|
|
case UnitActionType::BuildImprovement:
|
|
case UnitActionType::BuildCity:
|
|
case UnitActionType::Attack: {
|
|
continueCurrentUnitAction(unit, mapPoints, numPoints05);
|
|
return unit.currentAction.isValid() || unit.state != UnitState::Idle || unit.isDead();
|
|
}
|
|
case UnitActionType::BuildRoad: {
|
|
int tx = action.targetX;
|
|
int ty = action.targetY;
|
|
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));
|
|
unit.currentAction = UnitAction();
|
|
return false;
|
|
}
|
|
|
|
std::vector<std::pair<int, int>> route;
|
|
if (unit.tileX == tx && unit.tileY == ty) {
|
|
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));
|
|
unit.currentAction = UnitAction();
|
|
return false;
|
|
}
|
|
route.push_back({tx, ty});
|
|
} else {
|
|
route = Pathfinder::findPath(
|
|
unit.tileX, unit.tileY, tx, ty,
|
|
mapPoints, numPoints05, MovementDomain::Land, &unit.config
|
|
);
|
|
}
|
|
|
|
if (route.empty()) {
|
|
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
|
unit.currentAction = UnitAction();
|
|
return false;
|
|
}
|
|
|
|
unit.startRoadRoute(route);
|
|
if (route.size() > 1) {
|
|
addFloatingText(Localization::instance().get("ui.unit.road_route_started", "Building road route to (%1, %2)...").arg(tx).arg(ty),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.2f);
|
|
}
|
|
advanceRoadRoute(unit, mapPoints, numPoints05);
|
|
return true;
|
|
}
|
|
default:
|
|
unit.currentAction = UnitAction();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void GameManager::continueCurrentUnitAction(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
|
{
|
|
if (unit.isDead() || unit.state != UnitState::Idle || !unit.currentAction.isValid()) {
|
|
return;
|
|
}
|
|
|
|
UnitAction action = unit.currentAction;
|
|
|
|
switch (action.type) {
|
|
case UnitActionType::Move: {
|
|
// Movement finished
|
|
unit.currentAction = UnitAction();
|
|
break;
|
|
}
|
|
case UnitActionType::BuildImprovement: {
|
|
if (!unit.config.canBuildFarm()) {
|
|
unit.currentAction = UnitAction();
|
|
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));
|
|
unit.currentAction = UnitAction();
|
|
return;
|
|
}
|
|
|
|
int tx = action.targetX;
|
|
int ty = action.targetY;
|
|
QString impId = action.extraData.isEmpty() ? "farm" : action.extraData;
|
|
|
|
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));
|
|
unit.currentAction = UnitAction();
|
|
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));
|
|
unit.currentAction = UnitAction();
|
|
return;
|
|
}
|
|
|
|
// First move onto the target tile if not standing on it yet
|
|
if (unit.tileX != tx || unit.tileY != ty) {
|
|
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_to_build_farm", "Moving to build Farm at (%1, %2)...").arg(tx).arg(ty),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
|
|
} 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);
|
|
unit.currentAction = UnitAction();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Unit is on the target tile: start constructing the improvement
|
|
float ticks = unit.config.getActionDuration("boerderij_bouwen", 2.0f);
|
|
float duration = ticks * m_movementTickDuration;
|
|
unit.startBuilding(impId, duration);
|
|
addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm... (%1 ticks)").arg(ticks, 0, 'g', 3),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
|
|
break;
|
|
}
|
|
case UnitActionType::BuildCity: {
|
|
if (!unit.config.canBuildCity()) {
|
|
unit.currentAction = UnitAction();
|
|
return;
|
|
}
|
|
|
|
int tx = action.targetX;
|
|
int ty = action.targetY;
|
|
|
|
if (getCityAt(tx, ty) != nullptr) {
|
|
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));
|
|
unit.currentAction = UnitAction();
|
|
return;
|
|
}
|
|
|
|
// First move onto the target tile if not standing on it yet
|
|
if (unit.tileX != tx || unit.tileY != ty) {
|
|
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_to_build_city", "Moving to found City at (%1, %2)...").arg(tx).arg(ty),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
|
|
} 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);
|
|
unit.currentAction = UnitAction();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Unit is on the target tile: found the city!
|
|
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.currentAction = UnitAction();
|
|
unit.takeDamage(99999.0f);
|
|
if (unit.id == m_selectedUnitId) {
|
|
m_selectedUnitId = -1;
|
|
m_actionMode = ActionMode::None;
|
|
}
|
|
break;
|
|
}
|
|
case UnitActionType::Attack: {
|
|
Unit *target = getUnitById(action.targetUnitId);
|
|
if (!target || target->isDead() || target->playerId == unit.playerId || !unit.config.canAttack()) {
|
|
unit.currentAction = UnitAction();
|
|
return;
|
|
}
|
|
|
|
int dist = Pathfinder::hexDistance(unit.tileX, unit.tileY, target->tileX, target->tileY);
|
|
if (dist <= unit.config.attackRange) {
|
|
// Within range: execute attack!
|
|
float damage = (float)unit.config.attackDamage;
|
|
unit.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);
|
|
}
|
|
unit.currentAction = UnitAction();
|
|
return;
|
|
}
|
|
|
|
// Out of range: move into attackRange first
|
|
if (!mapPoints) {
|
|
unit.currentAction = UnitAction();
|
|
return;
|
|
}
|
|
|
|
std::vector<std::pair<int, int>> fullPath = Pathfinder::findPath(
|
|
unit.tileX, unit.tileY, target->tileX, target->tileY,
|
|
mapPoints, numPoints05, unit.config.movementDomain, &unit.config
|
|
);
|
|
std::vector<std::pair<int, int>> approachPath;
|
|
for (const auto &wp : fullPath) {
|
|
approachPath.push_back(wp);
|
|
if (Pathfinder::hexDistance(wp.first, wp.second, target->tileX, target->tileY) <= unit.config.attackRange) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (approachPath.size() > 1) {
|
|
unit.currentAction.targetX = approachPath.back().first;
|
|
unit.currentAction.targetY = approachPath.back().second;
|
|
unit.setPath(approachPath);
|
|
addFloatingText(Localization::instance().get("ui.unit.moving_to_attack", "Moving into attack range..."),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 190, 80), 2.0f);
|
|
} 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);
|
|
unit.currentAction = UnitAction();
|
|
}
|
|
break;
|
|
}
|
|
case UnitActionType::BuildRoad: {
|
|
if (!unit.hasRoadRoute()) {
|
|
unit.currentAction = UnitAction();
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
unit.currentAction = UnitAction();
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05, bool appendWithShift)
|
|
{
|
|
Unit *selected = getSelectedUnit();
|
|
if (!selected) return false;
|
|
|
|
int originX = selected->tileX;
|
|
int originY = selected->tileY;
|
|
getPlannedOriginTile(*selected, appendWithShift, originX, originY);
|
|
|
|
if (targetX == originX && targetY == originY) {
|
|
if (!appendWithShift && selected->hasQueuedAction()) {
|
|
selected->clearActionQueue();
|
|
addFloatingText(Localization::instance().get("ui.unit.queue_cleared", "Queued actions cancelled"),
|
|
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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 false;
|
|
}
|
|
|
|
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
|
|
originX, originY, targetX, targetY, mapPoints, numPoints05,
|
|
selected->config.movementDomain, &selected->config
|
|
);
|
|
|
|
if (path.size() <= 1) {
|
|
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
|
|
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
|
return false;
|
|
}
|
|
|
|
return scheduleOrStartUnitAction(*selected, UnitAction(UnitActionType::Move, targetX, targetY),
|
|
appendWithShift, mapPoints, numPoints05);
|
|
}
|
|
|
|
bool GameManager::commandAttack(Unit *target, Matrix2D<TerrainPoint> *mapPoints, int numPoints05, bool appendWithShift)
|
|
{
|
|
Unit *selected = getSelectedUnit();
|
|
if (!selected || !target || !selected->config.canAttack()) return false;
|
|
if (target->playerId == selected->playerId || target->isDead()) return false;
|
|
|
|
int originX = selected->tileX;
|
|
int originY = selected->tileY;
|
|
getPlannedOriginTile(*selected, appendWithShift, originX, originY);
|
|
|
|
int dist = Pathfinder::hexDistance(originX, originY, target->tileX, target->tileY);
|
|
int standX = originX;
|
|
int standY = originY;
|
|
|
|
if (dist > selected->config.attackRange) {
|
|
if (!mapPoints) {
|
|
addFloatingText(Localization::instance().get("ui.unit.out_of_range", "Out of range!"),
|
|
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
|
return false;
|
|
}
|
|
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
|
|
originX, originY, target->tileX, target->tileY,
|
|
mapPoints, numPoints05, selected->config.movementDomain, &selected->config
|
|
);
|
|
if (path.size() <= 1) {
|
|
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
|
|
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
|
return false;
|
|
}
|
|
for (const auto &wp : path) {
|
|
if (Pathfinder::hexDistance(wp.first, wp.second, target->tileX, target->tileY) <= selected->config.attackRange) {
|
|
standX = wp.first;
|
|
standY = wp.second;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return scheduleOrStartUnitAction(*selected, UnitAction(UnitActionType::Attack, standX, standY, "", target->id),
|
|
appendWithShift, mapPoints, numPoints05);
|
|
}
|
|
|
|
bool GameManager::commandBuildCityAtTileSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05, bool appendWithShift)
|
|
{
|
|
Unit *selected = getSelectedUnit();
|
|
if (!selected || !selected->config.canBuildCity()) return false;
|
|
|
|
if (getCityAt(targetX, targetY) != nullptr) {
|
|
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 false;
|
|
}
|
|
|
|
if (!TerrainMovementManager::instance().isHexPassable(
|
|
targetX, targetY, selected->config.movementDomain, mapPoints, numPoints05, &selected->config)) {
|
|
addFloatingText(Localization::instance().get("ui.unit.impassable_terrain", "Impassable terrain!"),
|
|
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
|
return false;
|
|
}
|
|
|
|
int originX = selected->tileX;
|
|
int originY = selected->tileY;
|
|
getPlannedOriginTile(*selected, appendWithShift, originX, originY);
|
|
|
|
if (targetX != originX || targetY != originY) {
|
|
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
|
|
originX, originY, targetX, targetY, mapPoints, numPoints05,
|
|
selected->config.movementDomain, &selected->config
|
|
);
|
|
if (path.size() <= 1) {
|
|
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
|
|
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return scheduleOrStartUnitAction(*selected, UnitAction(UnitActionType::BuildCity, targetX, targetY),
|
|
appendWithShift, mapPoints, numPoints05);
|
|
}
|
|
|
|
void GameManager::commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
|
{
|
|
Unit *selected = getSelectedUnit();
|
|
if (!selected) return;
|
|
commandBuildCityAtTileSelected(selected->tileX, selected->tileY, mapPoints, numPoints05, false);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool GameManager::commandBuildFarmAtTileSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05, bool appendWithShift)
|
|
{
|
|
Unit *selected = getSelectedUnit();
|
|
if (!selected || !selected->config.canBuildFarm()) return false;
|
|
|
|
if (!selected->config.infiniteCharges) {
|
|
int plannedFarms = 0;
|
|
if (selected->currentAction.type == UnitActionType::BuildImprovement) {
|
|
plannedFarms++;
|
|
}
|
|
if (appendWithShift) {
|
|
for (const auto &act : selected->actionQueue) {
|
|
if (act.type == UnitActionType::BuildImprovement) plannedFarms++;
|
|
}
|
|
}
|
|
if (selected->buildCharges - plannedFarms <= 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 false;
|
|
}
|
|
}
|
|
|
|
bool alreadyPlannedHere = false;
|
|
if (selected->currentAction.type == UnitActionType::BuildImprovement &&
|
|
selected->currentAction.targetX == targetX && selected->currentAction.targetY == targetY) {
|
|
alreadyPlannedHere = true;
|
|
}
|
|
if (appendWithShift && selected->hasQueuedImprovementAt(targetX, targetY)) {
|
|
alreadyPlannedHere = true;
|
|
}
|
|
|
|
if (hasImprovementAt(targetX, targetY) || alreadyPlannedHere) {
|
|
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 false;
|
|
}
|
|
|
|
QString tType = TerrainYieldManager::instance().getTerrainType(targetX, targetY, 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 false;
|
|
}
|
|
|
|
int originX = selected->tileX;
|
|
int originY = selected->tileY;
|
|
getPlannedOriginTile(*selected, appendWithShift, originX, originY);
|
|
|
|
if (targetX != originX || targetY != originY) {
|
|
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
|
|
originX, originY, targetX, targetY, mapPoints, numPoints05,
|
|
selected->config.movementDomain, &selected->config
|
|
);
|
|
if (path.size() <= 1) {
|
|
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
|
|
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return scheduleOrStartUnitAction(*selected, UnitAction(UnitActionType::BuildImprovement, targetX, targetY, "farm"),
|
|
appendWithShift, mapPoints, numPoints05);
|
|
}
|
|
|
|
void GameManager::commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
|
{
|
|
Unit *selected = getSelectedUnit();
|
|
if (!selected) return;
|
|
commandBuildFarmAtTileSelected(selected->tileX, selected->tileY, mapPoints, numPoints05, false);
|
|
}
|
|
|
|
void GameManager::commandBuildRoadSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
|
{
|
|
Unit *selected = getSelectedUnit();
|
|
if (!selected) return;
|
|
commandBuildRoadToTileSelected(selected->tileX, selected->tileY, mapPoints, numPoints05, false);
|
|
}
|
|
|
|
bool GameManager::commandBuildRoadToTileSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05, bool appendWithShift)
|
|
{
|
|
Unit *selected = getSelectedUnit();
|
|
if (!selected || !selected->config.canBuildRoad()) return false;
|
|
|
|
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 false;
|
|
}
|
|
|
|
int originX = selected->tileX;
|
|
int originY = selected->tileY;
|
|
getPlannedOriginTile(*selected, appendWithShift, originX, originY);
|
|
|
|
if (targetX == originX && targetY == originY) {
|
|
if (hasRoadAt(targetX, targetY)) {
|
|
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 false;
|
|
}
|
|
} else {
|
|
std::vector<std::pair<int, int>> route = Pathfinder::findPath(
|
|
originX, originY, targetX, targetY,
|
|
mapPoints, numPoints05, MovementDomain::Land, &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 false;
|
|
}
|
|
}
|
|
|
|
QString defaultType = m_roadConfigManager.defaultRoadType();
|
|
return scheduleOrStartUnitAction(*selected, UnitAction(UnitActionType::BuildRoad, targetX, targetY, defaultType),
|
|
appendWithShift, mapPoints, numPoints05);
|
|
}
|
|
|
|
void GameManager::advanceRoadRoute(Unit &unit, Matrix2D<TerrainPoint> *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) {
|
|
std::vector<std::pair<int, int>> stepPath = Pathfinder::findPath(
|
|
unit.tileX, unit.tileY, tx, ty, mapPoints, numPoints05,
|
|
MovementDomain::Land, &unit.config
|
|
);
|
|
if (stepPath.size() > 1) {
|
|
unit.setPath(stepPath);
|
|
} else {
|
|
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";
|
|
float ticks = unit.config.getActionDuration("build_road", (float)m_roadConfigManager.buildDurationTicks());
|
|
float duration = 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, 0, 'g', 3),
|
|
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
|
|
return;
|
|
}
|
|
|
|
// Finished entire route!
|
|
unit.clearRoadRoute();
|
|
if (unit.currentAction.type == UnitActionType::BuildRoad) {
|
|
unit.currentAction = UnitAction();
|
|
}
|
|
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<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);
|
|
|
|
if (unit.hasRoadRoute()) {
|
|
unit.advanceRoadRouteStep();
|
|
advanceRoadRoute(unit, mapPoints, numPoints05);
|
|
} else if (unit.currentAction.type == UnitActionType::BuildRoad) {
|
|
unit.currentAction = UnitAction();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (hasImprovementAt(tx, ty)) {
|
|
unit.cancelBuilding();
|
|
if (unit.currentAction.type == UnitActionType::BuildImprovement) {
|
|
unit.currentAction = UnitAction();
|
|
}
|
|
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.currentAction.type == UnitActionType::BuildImprovement) {
|
|
unit.currentAction = UnitAction();
|
|
}
|
|
|
|
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)
|
|
{
|
|
while (!unit.isDead() && unit.state == UnitState::Idle && !unit.currentAction.isValid() && unit.hasQueuedAction()) {
|
|
UnitAction nextAction = unit.popNextQueuedAction();
|
|
startUnitAction(unit, nextAction, mapPoints, numPoints05);
|
|
}
|
|
}
|
|
|
|
|