Implement technology tree, research queue by least remaining work, and unit/building tech availability & obsolescence
This commit is contained in:
+403
-5
@@ -14,7 +14,7 @@ GameManager& GameManager::instance()
|
||||
}
|
||||
|
||||
GameManager::GameManager()
|
||||
: m_selectedUnitId(-1), m_activeCityViewId(-1), m_actionMode(ActionMode::None),
|
||||
: 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(5), m_autoQueueMoveWhenBusy(true), m_allowActionQueueing(true),
|
||||
@@ -58,6 +58,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
||||
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);
|
||||
@@ -65,9 +69,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
||||
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 = cCfg.initialNationalResources.value("kennis", 0);
|
||||
player.science = 0;
|
||||
player.faith = cCfg.initialNationalResources.value("geloof", 0);
|
||||
player.gold = cCfg.initialNationalResources.value("goud", 0);
|
||||
}
|
||||
@@ -86,6 +91,11 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
||||
|
||||
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);
|
||||
@@ -123,6 +133,11 @@ Unit* GameManager::spawnUnit(const QString &configId, int playerId, int tileX, i
|
||||
|
||||
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) {
|
||||
@@ -171,8 +186,347 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
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()
|
||||
@@ -251,6 +605,7 @@ const City* GameManager::getActiveCityView() const
|
||||
void GameManager::enterCityView(int cityId)
|
||||
{
|
||||
m_activeCityViewId = cityId;
|
||||
m_isTechTreeOpen = false;
|
||||
m_selectedUnitId = -1;
|
||||
m_actionMode = ActionMode::None;
|
||||
}
|
||||
@@ -378,6 +733,7 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
|
||||
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);
|
||||
@@ -396,13 +752,13 @@ void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPo
|
||||
city.accumulatedLabor += yields.labor;
|
||||
}
|
||||
|
||||
// 3. National resources go to the owner player:
|
||||
// 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->science += yields.science;
|
||||
owner->faith += yields.faith;
|
||||
owner->gold += yields.gold;
|
||||
playerTickScience[city.playerId] += yields.science;
|
||||
}
|
||||
|
||||
// Floating growth text over city
|
||||
@@ -412,6 +768,13 @@ void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPo
|
||||
.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
|
||||
@@ -471,6 +834,7 @@ void GameManager::queueCityProject(int cityId, CityProjectType type, const QStri
|
||||
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;
|
||||
|
||||
@@ -478,6 +842,23 @@ void GameManager::queueCityProject(int cityId, CityProjectType type, const QStri
|
||||
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")
|
||||
@@ -505,6 +886,23 @@ void GameManager::queueCityProject(int cityId, CityProjectType type, const QStri
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user