Complete city production queue implementation

This commit is contained in:
bart
2026-11-08 22:30:13 +01:00
parent 4eda595861
commit 980e777199
14 changed files with 222 additions and 20 deletions
+75 -1
View File
@@ -55,6 +55,7 @@ public:
int accumulatedLabor = 0; // Local resource (Arbeid)
CityProject currentProject;
std::vector<CityProject> projectQueue;
std::vector<QString> completedBuildings; // IDs of completed buildings in the city
std::vector<std::pair<int, int>> territory; // Owned hex coordinates
@@ -77,6 +78,34 @@ public:
return true;
}
bool isBuildingQueuedOrBuilt(const QString &buildingId) const {
if (hasBuilding(buildingId)) return true;
if (currentProject.isValid() && currentProject.type == CityProjectType::Building && currentProject.id == buildingId) return true;
for (const auto &q : projectQueue) {
if (q.type == CityProjectType::Building && q.id == buildingId) return true;
}
return false;
}
int getQueuedBuildingPosition(const QString &buildingId) const {
for (size_t i = 0; i < projectQueue.size(); ++i) {
if (projectQueue[i].type == CityProjectType::Building && projectQueue[i].id == buildingId) {
return (int)i + 1;
}
}
return 0;
}
int getQueuedUnitCount(const QString &unitId) const {
int count = 0;
for (const auto &q : projectQueue) {
if (q.type == CityProjectType::Unit && q.id == unitId) {
count++;
}
}
return count;
}
bool hasActiveProject() const {
return currentProject.isValid();
}
@@ -90,8 +119,53 @@ public:
currentProject.district = district.isEmpty() ? "center" : district;
}
bool queueProject(CityProjectType type, const QString &projId, const QString &projName, int cost, const QString &district = "center", int maxQueueSize = 6) {
if (!hasActiveProject()) {
startProject(type, projId, projName, cost, district);
return true;
}
if ((int)projectQueue.size() >= maxQueueSize) {
return false;
}
CityProject q;
q.type = type;
q.id = projId;
q.name = projName;
q.laborCost = std::max(1, cost);
q.currentLabor = 0;
q.district = district.isEmpty() ? "center" : district;
projectQueue.push_back(q);
return true;
}
void cancelActiveProject() {
if (!projectQueue.empty()) {
currentProject = projectQueue.front();
projectQueue.erase(projectQueue.begin());
} else {
currentProject = CityProject();
}
}
void cancelProject() {
currentProject = CityProject();
cancelActiveProject();
}
bool removeQueuedProject(int index) {
if (index >= 0 && index < (int)projectQueue.size()) {
projectQueue.erase(projectQueue.begin() + index);
return true;
}
return false;
}
void advanceQueue() {
if (!projectQueue.empty()) {
currentProject = projectQueue.front();
projectQueue.erase(projectQueue.begin());
} else {
currentProject = CityProject();
}
}
bool addTerritory(int tx, int ty) {
+1
View File
@@ -14,6 +14,7 @@ tile_culture_cost: 10000
food_to_inhabitants_ratio: 1.0
default_unit_labor_cost: 1000
unit_labor_cost_multiplier: 20
max_production_queue_size: 6
# Base yield breakdown per resource (default 500 per tick)
base_yields:
+3
View File
@@ -12,6 +12,7 @@ types:
color: "#c49a62"
border_color: "#6b4b23"
width: 3.5
world_width: 0.11
cost: 5
stone:
id: "stone"
@@ -20,6 +21,7 @@ types:
color: "#9aa1a8"
border_color: "#464b50"
width: 4.0
world_width: 0.13
cost: 15
rail:
id: "rail"
@@ -28,4 +30,5 @@ types:
color: "#3a3c40"
border_color: "#181a1c"
width: 4.5
world_width: 0.15
cost: 30
+20 -5
View File
@@ -102,7 +102,7 @@ ui:
tip_build_farm: "Build Farm (F)"
tip_farm_building: "Busy building... %1%"
tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)"
tip_build_road: "Build Road (R)"
tip_build_road: "Build Road (R) - Click target tile to build route"
tip_road_building: "Busy building road... %1%"
tip_road_queued: "Build Road (Queued on arrival - click to cancel)"
road_started: "Constructing %1... (%2 ticks)"
@@ -110,6 +110,11 @@ ui:
road_queued: "Build Road queued on arrival"
road_already_here: "Tile already has a road!"
cannot_build_road_water: "Roads cannot be built on water!"
road_route_started: "Building road route to (%1, %2)..."
road_route_completed: "Road route completed!"
road_route_cancelled: "Road route cancelled"
status_road_route: "Road Route: %1/%2"
btn_road_route: "Route (%1/%2)"
improvement:
farm: "Farm"
@@ -126,25 +131,35 @@ ui:
national_info: "National: Culture: %L1 | Science: %L2 | Gold: %L3 | Faith: %L4"
help_text: "Click a slot to assign/free Pop | Hold a slot to choose resource | Buy tile with Culture."
current_production: "CURRENT PRODUCTION"
production_panel_title: "CITY PRODUCTION"
no_project: "No active project"
select_prompt: "Select a unit or building to construct"
select_prompt: "Select an item below to construct"
ticks_remaining: "%1 ticks remaining (%2 / %3 Labor)"
cancel_btn: "Cancel"
queue_header: "QUEUE (%1 / %2)"
queue_empty: "Queue is empty"
districts_header: "DISTRICT: CITY CENTER"
no_buildings: "No extensions built yet"
choose_project: "CHOOSE CITY PROJECT"
tab_buildings: "Buildings & Extensions"
choose_project: "AVAILABLE PROJECTS"
tab_buildings: "Buildings"
tab_units: "Units"
btn_build: "Build"
btn_train: "Train"
btn_queue: "+ Queue"
status_built: "BUILT"
status_in_progress: "ACTIVE"
status_queued: "QUEUED #%1"
status_full: "FULL"
queue_full_float: "Production queue is full! (Max %1)"
labor_cost_label: "%1 Labor (%2 ticks)"
unit_trained: "%1: %2 trained!"
building_completed: "%1: %2 completed in %3!"
project_started: "%1: Started %2"
project_cancelled: "%1: Project cancelled"
project_queued: "%1: Added %2 to queue (#%3)"
project_cancelled: "%1: Cancelled %2"
queued_item_removed: "%1: Removed %2 from queue"
already_built: "%1 already built in this city!"
already_queued: "%1 is already in the production queue!"
growth_float: "+%1 Inhabitants | +%2 Culture"
not_enough_culture: "Not enough Culture! (%1 needed)"
worker_freed: "Pop worker freed"
+87 -14
View File
@@ -455,41 +455,106 @@ bool GameManager::buyTileForCity(int cityId, int tx, int ty)
return true;
}
void GameManager::setCityProject(int cityId, CityProjectType type, const QString &id)
void GameManager::queueCityProject(int cityId, CityProjectType type, const QString &id)
{
City *city = getCityById(cityId);
if (!city) return;
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
int maxQueue = cCfg.maxProductionQueueSize;
if (type == CityProjectType::Unit) {
const UnitConfig *uCfg = m_unitConfigManager.getConfig(id);
if (!uCfg) return;
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);
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!").arg(bCfg->name);
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;
}
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);
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 msg = Localization::instance().get("ui.city.project_cancelled", "%1: Project cancelled").arg(city->name);
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);
city->cancelProject();
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)
@@ -540,8 +605,16 @@ void GameManager::completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoi
}
int excess = city.currentProject.currentLabor - city.currentProject.laborCost;
city.accumulatedLabor += std::max(0, excess);
city.cancelProject();
city.advanceQueue();
if (city.hasActiveProject() && excess > 0) {
city.currentProject.currentLabor += excess;
if (city.currentProject.currentLabor >= city.currentProject.laborCost) {
completeCityProject(city, mapPoints, numPoints05);
}
} else {
city.accumulatedLabor += std::max(0, excess);
}
}
void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
+2
View File
@@ -108,8 +108,10 @@ public:
bool isTileOwned(int tx, int ty, int *outOwnerCityId = nullptr) const;
bool buyTileForCity(int cityId, int tx, int ty);
void queueCityProject(int cityId, CityProjectType type, const QString &id);
void setCityProject(int cityId, CityProjectType type, const QString &id);
void cancelCityProject(int cityId);
void removeQueuedCityProject(int cityId, int queueIndex);
void completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

+6
View File
@@ -143,6 +143,12 @@ protected:
};
std::vector<CityProjectHit> m_cityProjectHits;
struct CityQueueItemHit {
QRect rect;
int queueIndex;
};
std::vector<CityQueueItemHit> m_cityQueueItemHits;
QRect m_cancelProjectBtnRect;
int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units
QRect m_tabBuildingsRect;
+4
View File
@@ -17,6 +17,7 @@ RoadConfigManager::RoadConfigManager()
dirt.colorStr = "#c49a62";
dirt.borderColorStr = "#6b4b23";
dirt.width = 3.5f;
dirt.worldWidth = 0.11f;
dirt.cost = 5;
m_types["dirt"] = dirt;
@@ -27,6 +28,7 @@ RoadConfigManager::RoadConfigManager()
stone.colorStr = "#9aa1a8";
stone.borderColorStr = "#464b50";
stone.width = 4.0f;
stone.worldWidth = 0.13f;
stone.cost = 15;
m_types["stone"] = stone;
@@ -37,6 +39,7 @@ RoadConfigManager::RoadConfigManager()
rail.colorStr = "#3a3c40";
rail.borderColorStr = "#181a1c";
rail.width = 4.5f;
rail.worldWidth = 0.15f;
rail.cost = 30;
m_types["rail"] = rail;
}
@@ -72,6 +75,7 @@ bool RoadConfigManager::loadConfigFile(const QString &filePath)
rCfg.colorStr = it.value().hasChild("color") ? it.value()["color"].asString("#c49a62") : "#c49a62";
rCfg.borderColorStr = it.value().hasChild("border_color") ? it.value()["border_color"].asString("#6b4b23") : "#6b4b23";
rCfg.width = it.value().hasChild("width") ? it.value()["width"].asFloat(3.5f) : 3.5f;
rCfg.worldWidth = it.value().hasChild("world_width") ? it.value()["world_width"].asFloat(0.11f) : 0.11f;
rCfg.cost = it.value().hasChild("cost") ? it.value()["cost"].asInt(5) : 5;
m_types[rCfg.id] = rCfg;
}
+1
View File
@@ -12,6 +12,7 @@ struct RoadTypeConfig {
QString colorStr = "#c49a62";
QString borderColorStr = "#6b4b23";
float width = 3.5f;
float worldWidth = 0.11f;
int cost = 5;
QColor getColor() const { return QColor(colorStr); }
+1
View File
@@ -35,6 +35,7 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir)
m_cityConfig.baseResourceYieldPerTick = cityRoot["base_resource_yield_per_tick"].asInt(500);
m_cityConfig.tileCultureCost = cityRoot["tile_culture_cost"].asInt(10000);
m_cityConfig.foodToInhabitantsRatio = cityRoot["food_to_inhabitants_ratio"].asFloat(1.0f);
m_cityConfig.maxProductionQueueSize = cityRoot["max_production_queue_size"].asInt(6);
if (cityRoot.hasChild("base_yields")) {
const YamlNode &bNode = cityRoot["base_yields"];
+2
View File
@@ -34,6 +34,7 @@ struct CityConfig {
int baseResourceYieldPerTick;
int tileCultureCost;
float foodToInhabitantsRatio;
int maxProductionQueueSize;
int slotLongPressDurationMs;
QStringList resourcePriority;
@@ -47,6 +48,7 @@ struct CityConfig {
cityViewZoom(4.8f),
baseResourceYieldPerTick(500), tileCultureCost(10000),
foodToInhabitantsRatio(1.0f),
maxProductionQueueSize(6),
slotLongPressDurationMs(400),
resourcePriority({"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"})
{
+1
View File
@@ -123,6 +123,7 @@ void Unit::stopMovement()
moveProgress = 0.0f;
state = UnitState::Idle;
clearActionQueue();
clearRoadRoute();
}
void Unit::startBuilding(const QString &impId, float duration)
+19
View File
@@ -69,6 +69,25 @@ public:
std::deque<UnitAction> actionQueue;
int maxQueueSize = 5;
// Road Construction Route (tile-by-tile road paving)
std::vector<std::pair<int, int>> roadRoute;
size_t roadRouteIndex = 0;
bool hasRoadRoute() const { return !roadRoute.empty() && roadRouteIndex < roadRoute.size(); }
void startRoadRoute(const std::vector<std::pair<int, int>> &route) {
roadRoute = route;
roadRouteIndex = 0;
}
void clearRoadRoute() {
roadRoute.clear();
roadRouteIndex = 0;
}
void advanceRoadRouteStep() {
if (!roadRoute.empty()) {
roadRouteIndex++;
}
}
bool queueAction(const UnitAction &action);
void clearActionQueue();
bool hasQueuedAction() const { return !actionQueue.empty(); }