diff --git a/city.h b/city.h index 797fa84..d5bcfd7 100644 --- a/city.h +++ b/city.h @@ -55,6 +55,7 @@ public: int accumulatedLabor = 0; // Local resource (Arbeid) CityProject currentProject; + std::vector projectQueue; std::vector completedBuildings; // IDs of completed buildings in the city std::vector> 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) { diff --git a/config/city.yaml b/config/city.yaml index fec3a57..827eb47 100644 --- a/config/city.yaml +++ b/config/city.yaml @@ -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: diff --git a/config/roads.yaml b/config/roads.yaml index 755e303..8ef8636 100644 --- a/config/roads.yaml +++ b/config/roads.yaml @@ -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 diff --git a/config/strings_en.yaml b/config/strings_en.yaml index 0c49dfd..4e4bf3d 100644 --- a/config/strings_en.yaml +++ b/config/strings_en.yaml @@ -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" diff --git a/gamemanager.cpp b/gamemanager.cpp index 1aa2168..10cf386 100644 --- a/gamemanager.cpp +++ b/gamemanager.cpp @@ -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 *mapPoints, int numPoints05) @@ -540,8 +605,16 @@ void GameManager::completeCityProject(City &city, Matrix2D *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 *mapPoints, int numPoints05) diff --git a/gamemanager.h b/gamemanager.h index 62343fe..7d6cc2a 100644 --- a/gamemanager.h +++ b/gamemanager.h @@ -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 *mapPoints, int numPoints05); void commandMoveSelected(int targetX, int targetY, Matrix2D *mapPoints, int numPoints05); diff --git a/images/mine.jpeg b/images/mine.jpeg new file mode 100644 index 0000000..b334d27 Binary files /dev/null and b/images/mine.jpeg differ diff --git a/qscenerenderer.h b/qscenerenderer.h index edcb880..3471636 100644 --- a/qscenerenderer.h +++ b/qscenerenderer.h @@ -143,6 +143,12 @@ protected: }; std::vector m_cityProjectHits; + struct CityQueueItemHit { + QRect rect; + int queueIndex; + }; + std::vector m_cityQueueItemHits; + QRect m_cancelProjectBtnRect; int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units QRect m_tabBuildingsRect; diff --git a/roadconfig.cpp b/roadconfig.cpp index c2d224a..88fa7bc 100644 --- a/roadconfig.cpp +++ b/roadconfig.cpp @@ -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; } diff --git a/roadconfig.h b/roadconfig.h index 2d3b516..534373e 100644 --- a/roadconfig.h +++ b/roadconfig.h @@ -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); } diff --git a/terrainyieldmanager.cpp b/terrainyieldmanager.cpp index 2ab4677..66a7138 100644 --- a/terrainyieldmanager.cpp +++ b/terrainyieldmanager.cpp @@ -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"]; diff --git a/terrainyieldmanager.h b/terrainyieldmanager.h index 0437af6..64fd65e 100644 --- a/terrainyieldmanager.h +++ b/terrainyieldmanager.h @@ -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"}) { diff --git a/unit.cpp b/unit.cpp index a2b9690..773595d 100644 --- a/unit.cpp +++ b/unit.cpp @@ -123,6 +123,7 @@ void Unit::stopMovement() moveProgress = 0.0f; state = UnitState::Idle; clearActionQueue(); + clearRoadRoute(); } void Unit::startBuilding(const QString &impId, float duration) diff --git a/unit.h b/unit.h index 5fce932..23620a4 100644 --- a/unit.h +++ b/unit.h @@ -69,6 +69,25 @@ public: std::deque actionQueue; int maxQueueSize = 5; + // Road Construction Route (tile-by-tile road paving) + std::vector> roadRoute; + size_t roadRouteIndex = 0; + + bool hasRoadRoute() const { return !roadRoute.empty() && roadRouteIndex < roadRoute.size(); } + void startRoadRoute(const std::vector> &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(); }