diff --git a/city.h b/city.h index 6dc0b89..797fa84 100644 --- a/city.h +++ b/city.h @@ -59,6 +59,7 @@ public: std::vector> territory; // Owned hex coordinates std::map, QString> workedTiles; // (x, y) -> resource name + std::map, QString> tilePreferredResources; // (x, y) -> user chosen resource City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos, int initInhabitants = 1000, int initInhabitantsPerPop = 100000, int initRadius = 1); @@ -127,8 +128,24 @@ public: return it != workedTiles.end() ? it->second : ""; } + QString getEffectiveResource(int tx, int ty, const QString &defaultBestRes) const { + auto itW = workedTiles.find({tx, ty}); + if (itW != workedTiles.end()) return itW->second; + auto itP = tilePreferredResources.find({tx, ty}); + if (itP != tilePreferredResources.end()) return itP->second; + return defaultBestRes; + } + + void setTilePreferredResource(int tx, int ty, const QString &resource) { + tilePreferredResources[{tx, ty}] = resource; + if (isTileWorked(tx, ty)) { + workedTiles[{tx, ty}] = resource; + } + } + bool assignWorker(int tx, int ty, const QString &resource) { if (!ownsTile(tx, ty)) return false; + tilePreferredResources[{tx, ty}] = resource; if (isTileWorked(tx, ty)) { workedTiles[{tx, ty}] = resource; return true; diff --git a/config/city.yaml b/config/city.yaml index 88cf169..fec3a57 100644 --- a/config/city.yaml +++ b/config/city.yaml @@ -30,3 +30,15 @@ initial_national_resources: kennis: 0 geloof: 0 goud: 0 + +# Slot long press duration in milliseconds to open resource selection popup +slot_long_press_duration_ms: 400 + +# Priority order for resources (used for tie-breaking when yields are equal and display order) +resource_priority: + - "voedsel" + - "arbeid" + - "goud" + - "kennis" + - "cultuur" + - "geloof" diff --git a/config/strings_en.yaml b/config/strings_en.yaml index 6e86313..5631cbf 100644 --- a/config/strings_en.yaml +++ b/config/strings_en.yaml @@ -55,6 +55,11 @@ ui: farm_completed: "Farm completed!" farm_completed_consumed: "Farm completed! (Builder consumed)" farm_completed_charges: "Farm completed! (%1 %2 remaining)" + road_queued: "Build Road queued on arrival" + road_started: "Constructing %1... (%2 ticks)" + road_completed: "%1 completed!" + road_already_here: "Tile already has a road!" + cannot_build_road_water: "Roads cannot be built on water!" city_queued: "Found City queued on arrival" city_founded: "%1 founded!" already_building: "Already busy building!" @@ -88,12 +93,18 @@ ui: btn_build_farm: "Farm [F]" btn_farm_queued: "Farm (Queued)" btn_farm_building: "Building %1%" + btn_build_road: "Road [R]" + btn_road_queued: "Road (Queued)" + btn_road_building: "Road %1%" tip_move: "Move (M)" tip_attack: "Attack (A) [Range: %1]" tip_build_city: "Found City (B)" 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) [2 ticks]" + tip_road_building: "Constructing road... %1%" + tip_road_queued: "Build Road (Queued on arrival - click to cancel)" city: center: "City Center" @@ -104,7 +115,7 @@ ui: close_btn: "[X] Close" local_info: "Local: Population: %L1 (%2 Pop: %3 working, %4 free) | Labor: %L5" national_info: "National: Culture: %L1 | Science: %L2 | Gold: %L3 | Faith: %L4" - help_text: "Click a tile to assign 1 Pop | Click an unowned tile to buy with Culture." + help_text: "Click a slot to assign/free Pop | Hold a slot to choose resource | Buy tile with Culture." current_production: "CURRENT PRODUCTION" no_project: "No active project" select_prompt: "Select a unit or building to construct" @@ -132,3 +143,16 @@ ui: no_free_pop: "No free Pop!" buy_tile: "BUY" occupied: "OCCUPIED" + select_resource: "SELECT RESOURCE" + best_yield: "BEST" + resource_switched: "Producing %1 (+%2)" + worker_assigned_res: "+1 Pop: %1 (+%2)" + resource_preferred: "Slot set to %1 (+%2)" + + resources: + voedsel: "Food" + arbeid: "Production" + goud: "Gold" + kennis: "Science" + cultuur: "Culture" + geloof: "Faith" diff --git a/gamemanager.cpp b/gamemanager.cpp index 7687dab..96b7513 100644 --- a/gamemanager.cpp +++ b/gamemanager.cpp @@ -72,10 +72,8 @@ void GameManager::init(const QString &baseDir, Matrix2D *mapPoints City initialCity(cId, 1, "Roma", 35, 17, QVector3D(w.x(), w.y(), gz), cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius); - // Assign initial 1 pop to work the city center tile - QString tType = TerrainYieldManager::instance().getTerrainType(35, 17, mapPoints, numPoints05); - const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType); - QString defRes = yCfg ? yCfg->primaryResource : "voedsel"; + // 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); @@ -247,6 +245,67 @@ void GameManager::exitCityView() m_activeCityViewId = -1; } +QMap GameManager::getTileYields(int tx, int ty, Matrix2D *mapPoints, int numPoints05) const +{ + QMap yields; + const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); + QStringList resources = cCfg.resourcePriority; + if (resources.isEmpty()) { + resources = {"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 *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 *mapPoints, int numPoints05) const +{ + const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); + QStringList priority = cCfg.resourcePriority; + if (priority.isEmpty()) { + priority = {"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"}; + } + + QMap 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 *mapPoints, int numPoints05) const { CityYieldSummary summary; @@ -266,20 +325,7 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2DgetYield(assignedRes); - - // Add bonus from tile improvements (e.g. farm bonus) - const TileImprovement *imp = getImprovementAt(tx, ty); - if (imp) { - const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId); - if (iCfg && iCfg->yieldBonus.contains(assignedRes)) { - amount += iCfg->yieldBonus.value(assignedRes, 0); - } - } + int amount = getTileYield(tx, ty, assignedRes, mapPoints, numPoints05); if (assignedRes == "voedsel") summary.food += amount; else if (assignedRes == "arbeid") summary.labor += amount; @@ -632,10 +678,8 @@ void GameManager::commandBuildCitySelected(Matrix2D *mapPoints, in QVector3D(w.x(), w.y(), gz), cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius); - // Assign initial worker to center tile - QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); - const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType); - QString defRes = yCfg ? yCfg->primaryResource : "voedsel"; + // Assign initial worker to center tile with best yield resource + QString defRes = getBestTileResource(tx, ty, mapPoints, numPoints05); newCity.assignWorker(tx, ty, defRes); m_cities.push_back(newCity); @@ -907,9 +951,7 @@ void GameManager::processUnitActionQueue(Unit &unit, Matrix2D *map QVector3D(w.x(), w.y(), gz), cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius); - QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); - const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType); - QString defRes = yCfg ? yCfg->primaryResource : "voedsel"; + QString defRes = getBestTileResource(tx, ty, mapPoints, numPoints05); newCity.assignWorker(tx, ty, defRes); m_cities.push_back(newCity); diff --git a/gamemanager.h b/gamemanager.h index 1be2bef..c678e43 100644 --- a/gamemanager.h +++ b/gamemanager.h @@ -7,6 +7,7 @@ #include "player.h" #include "unitconfig.h" #include "buildingconfig.h" +#include "roadconfig.h" #include "unit.h" #include "city.h" #include "matrix2d.h" @@ -18,7 +19,8 @@ enum class ActionMode { Move, Attack, BuildCity, - BuildFarm + BuildFarm, + BuildRoad }; struct TileImprovement { @@ -29,6 +31,15 @@ struct TileImprovement { QVector3D worldPos; }; +struct TileRoad { + QString roadTypeId = "dirt"; + int tileX = 0; + int tileY = 0; + int playerId = 1; + QVector3D worldPos; + unsigned char connectionMask = 0; // bit e (0..5) indicates connected across edge e +}; + struct FloatingText { QString text; QVector3D worldPos; @@ -48,6 +59,8 @@ public: UnitConfigManager& unitConfigs() { return m_unitConfigManager; } BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; } const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; } + RoadConfigManager& roadConfigs() { return m_roadConfigManager; } + const RoadConfigManager& roadConfigs() const { return m_roadConfigManager; } const std::vector>& getUnits() const { return m_units; } const std::vector& getCities() const { return m_cities; } @@ -79,6 +92,10 @@ public: CityYieldSummary calculateCityYields(const City &city, Matrix2D *mapPoints, int numPoints05) const; void processTickYields(Matrix2D *mapPoints, int numPoints05); + QMap getTileYields(int tx, int ty, Matrix2D *mapPoints, int numPoints05) const; + int getTileYield(int tx, int ty, const QString &res, Matrix2D *mapPoints, int numPoints05) const; + QString getBestTileResource(int tx, int ty, Matrix2D *mapPoints, int numPoints05) const; + bool isTileOwned(int tx, int ty, int *outOwnerCityId = nullptr) const; bool buyTileForCity(int cityId, int tx, int ty); diff --git a/qscenerenderer.cpp b/qscenerenderer.cpp index 2bf8ab8..f59c47b 100644 --- a/qscenerenderer.cpp +++ b/qscenerenderer.cpp @@ -72,6 +72,12 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent) QTimer *animTimer = new QTimer(this); connect(animTimer, &QTimer::timeout, this, QOverload<>::of(&QSceneRenderer::update)); animTimer->start(16); + + m_slotLongPressTimer = new QTimer(this); + m_slotLongPressTimer->setSingleShot(true); + connect(m_slotLongPressTimer, &QTimer::timeout, this, [this]() { + onSlotLongPressTimeout(); + }); } void QSceneRenderer::loadCameraConfig() @@ -1190,15 +1196,10 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM if (yCfg && yCfg->workable) { // Tile has 1 slot! bool isWorked = city->isTileWorked(hx, hy); - QString res = isWorked ? city->getWorkedResource(hx, hy) : yCfg->primaryResource; - int amount = yCfg->getYield(res); + QString bestRes = GameManager::instance().getBestTileResource(hx, hy, m_point, m_numPoints05); + QString res = city->getEffectiveResource(hx, hy, bestRes); + int amount = GameManager::instance().getTileYield(hx, hy, res, m_point, m_numPoints05); const TileImprovement *imp = GameManager::instance().getImprovementAt(hx, hy); - if (imp) { - const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId); - if (iCfg && iCfg->yieldBonus.contains(res)) { - amount += iCfg->yieldBonus.value(res, 0); - } - } float slotR = 15.0f; QRect slotRect((int)(sCenter.x() - slotR), (int)(sCenter.y() - slotR), (int)(slotR * 2), (int)(slotR * 2)); @@ -1209,6 +1210,24 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM painter.setPen(QPen(isWorked ? QColor(80, 240, 120) : QColor(140, 175, 215), isWorked ? 2.2 : 1.2)); painter.drawEllipse(sCenter, slotR, slotR); + // Long-press charging progress ring on the active held slot + if (m_slotPressPending && m_pressedSlotTx == hx && m_pressedSlotTy == hy) { + int dur = TerrainYieldManager::instance().getCityConfig().slotLongPressDurationMs; + float charge = std::min(1.0f, (float)m_slotPressElapsed.elapsed() / (float)std::max(1, dur)); + QRectF chargeRect(sCenter.x() - (slotR + 3.0f), sCenter.y() - (slotR + 3.0f), (slotR + 3.0f) * 2, (slotR + 3.0f) * 2); + painter.setPen(QPen(QColor(255, 220, 80, 230), 2.5)); + painter.setBrush(Qt::NoBrush); + painter.drawArc(chargeRect, 90 * 16, -(int)(charge * 360 * 16)); + } + + // Active menu highlight ring + if (m_resourceMenuOpen && m_resourceMenuTx == hx && m_resourceMenuTy == hy) { + QRectF activeMenuRect(sCenter.x() - (slotR + 3.0f), sCenter.y() - (slotR + 3.0f), (slotR + 3.0f) * 2, (slotR + 3.0f) * 2); + painter.setPen(QPen(QColor(80, 200, 255, 240), 2.0)); + painter.setBrush(Qt::NoBrush); + painter.drawEllipse(activeMenuRect); + } + // Resource icon QImage rIcon = getTintedIcon("images/resources/" + res + ".png", isWorked ? QColor(80, 255, 120) : QColor(180, 210, 245)); painter.drawImage(QRectF(sCenter.x() - 8.5, sCenter.y() - 8.5, 17, 17), rIcon); @@ -1712,6 +1731,188 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM idx++; } } + + // 4. Resource Selection Popup Menu (if open) + if (m_resourceMenuOpen) { + drawCityResourceMenu(painter, vMatrix); + } +} + +void QSceneRenderer::onSlotLongPressTimeout() +{ + if (m_slotPressPending && GameManager::instance().isInCityView()) { + m_slotLongPressTriggered = true; + m_slotPressPending = false; + m_resourceMenuOpen = true; + m_resourceMenuTx = m_pressedSlotTx; + m_resourceMenuTy = m_pressedSlotTy; + update(); + } +} + +void QSceneRenderer::drawCityResourceMenu(QPainter &painter, const QMatrix4x4 &vMatrix) +{ + m_cityResourceChoiceHits.clear(); + m_resourceMenuCloseBtnRect = QRect(); + m_resourceMenuRect = QRect(); + + City *city = GameManager::instance().getActiveCityView(); + if (!city) { + m_resourceMenuOpen = false; + return; + } + + int tx = m_resourceMenuTx; + int ty = m_resourceMenuTy; + if (!city->ownsTile(tx, ty)) { + m_resourceMenuOpen = false; + return; + } + + QVector2D center = Pathfinder::hexToWorld(tx, ty); + float gz = Unit::getTerrainHeight(center.x(), center.y(), m_point, m_numPoints05); + bool inFront = false; + QPointF sCenter = UnitRenderer::projectToScreen(QVector3D(center.x(), center.y(), gz + 0.05f), Projection, vMatrix, width(), height(), inFront); + if (!inFront) return; + + QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, m_point, m_numPoints05); + const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType); + QString tName = yCfg ? yCfg->name : tType; + const TileImprovement *imp = GameManager::instance().getImprovementAt(tx, ty); + if (imp) { + const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId); + if (iCfg) { + tName += QString(" (%1)").arg(iCfg->name); + } + } + + const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); + QStringList resources = cCfg.resourcePriority; + if (resources.isEmpty()) { + resources = {"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"}; + } + + QString bestRes = GameManager::instance().getBestTileResource(tx, ty, m_point, m_numPoints05); + QString activeRes = city->getEffectiveResource(tx, ty, bestRes); + + int menuW = 240; + int rowH = 32; + int headerH = 46; + int footerH = 10; + int menuH = headerH + (resources.size() * rowH) + footerH; + + // Anchor menu near sCenter + int menuX = (int)sCenter.x() + 24; + int menuY = (int)sCenter.y() - menuH / 2; + if (menuX + menuW > width() - 15) { + menuX = (int)sCenter.x() - menuW - 24; + } + if (menuX < 15) menuX = 15; + if (menuY + menuH > height() - 15) { + menuY = height() - 15 - menuH; + } + if (menuY < 15) menuY = 15; + + m_resourceMenuRect = QRect(menuX, menuY, menuW, menuH); + + // Connecting line from slot to menu card + painter.setPen(QPen(QColor(80, 200, 255, 120), 1.5, Qt::DashLine)); + painter.drawLine(sCenter, QPointF(menuX < sCenter.x() ? menuX + menuW : menuX, menuY + menuH / 2)); + + // Drop shadow + painter.setBrush(QColor(0, 0, 0, 160)); + painter.setPen(Qt::NoPen); + painter.drawRoundedRect(m_resourceMenuRect.adjusted(4, 4, 4, 4), 8, 8); + + // Card background + painter.setBrush(QColor(16, 22, 32, 250)); + painter.setPen(QPen(QColor(80, 180, 240), 1.6)); + painter.drawRoundedRect(m_resourceMenuRect, 8, 8); + + // Header Title + painter.setFont(QFont("sans-serif", 9, QFont::Bold)); + painter.setPen(QColor(240, 248, 255)); + QString title = Localization::instance().get("ui.city.select_resource", "SELECT RESOURCE"); + painter.drawText(QRect(menuX + 12, menuY + 6, menuW - 40, 18), Qt::AlignLeft | Qt::AlignVCenter, title); + + // Close button [X] + m_resourceMenuCloseBtnRect = QRect(menuX + menuW - 26, menuY + 6, 20, 18); + painter.setFont(QFont("sans-serif", 9, QFont::Bold)); + painter.setPen(QColor(180, 190, 205)); + painter.drawText(m_resourceMenuCloseBtnRect, Qt::AlignCenter, "×"); + + // Subtitle (tile info) + painter.setFont(QFont("sans-serif", 7)); + painter.setPen(QColor(140, 170, 200)); + painter.drawText(QRect(menuX + 12, menuY + 24, menuW - 24, 16), Qt::AlignLeft | Qt::AlignVCenter, tName); + + // Divider line + painter.setPen(QPen(QColor(50, 75, 105), 1.0)); + painter.drawLine(menuX + 10, menuY + 44, menuX + menuW - 10, menuY + 44); + + // Rows + int currY = menuY + headerH; + for (const QString &res : resources) { + int yieldVal = GameManager::instance().getTileYield(tx, ty, res, m_point, m_numPoints05); + bool isActive = (res == activeRes); + bool isBest = (res == bestRes); + + QRect rowRect(menuX + 8, currY, menuW - 16, rowH - 4); + m_cityResourceChoiceHits.push_back({rowRect, res, yieldVal}); + + bool isHovered = rowRect.contains(m_mouseCurrentPos); + + // Row background + if (isActive) { + painter.setBrush(QColor(30, 65, 45, 240)); + painter.setPen(QPen(QColor(80, 240, 120), 1.4)); + } else if (isHovered) { + painter.setBrush(QColor(40, 60, 85, 230)); + painter.setPen(QPen(QColor(100, 190, 255), 1.2)); + } else { + painter.setBrush(QColor(22, 30, 42, 210)); + painter.setPen(QPen(QColor(45, 60, 80), 1.0)); + } + painter.drawRoundedRect(rowRect, 5, 5); + + // Resource icon + QColor iconColor; + if (res == "voedsel") iconColor = QColor(90, 255, 130); + else if (res == "arbeid") iconColor = QColor(255, 160, 60); + else if (res == "goud") iconColor = QColor(255, 215, 60); + else if (res == "kennis") iconColor = QColor(80, 180, 255); + else if (res == "cultuur") iconColor = QColor(225, 110, 255); + else if (res == "geloof") iconColor = QColor(220, 230, 255); + else iconColor = QColor(200, 220, 240); + + QImage resIcon = getTintedIcon("images/resources/" + res + ".png", iconColor); + painter.drawImage(QRect(rowRect.left() + 6, rowRect.top() + (rowRect.height() - 18) / 2, 18, 18), resIcon); + + // Resource Name + QString resName = Localization::instance().get(QString("ui.resources.%1").arg(res), res); + painter.setFont(QFont("sans-serif", 8, isHovered || isActive ? QFont::Bold : QFont::Normal)); + painter.setPen(isActive ? QColor(100, 255, 150) : (isHovered ? QColor(255, 255, 255) : QColor(200, 215, 230))); + painter.drawText(QRect(rowRect.left() + 28, rowRect.top(), 80, rowRect.height()), Qt::AlignLeft | Qt::AlignVCenter, resName); + + // Best tag + if (isBest) { + QRect bestRect(rowRect.left() + 110, rowRect.top() + (rowRect.height() - 14) / 2, 34, 14); + painter.setBrush(QColor(80, 65, 20, 220)); + painter.setPen(QPen(QColor(255, 205, 50), 1.0)); + painter.drawRoundedRect(bestRect, 3, 3); + painter.setFont(QFont("sans-serif", 6, QFont::Bold)); + painter.setPen(QColor(255, 220, 80)); + painter.drawText(bestRect, Qt::AlignCenter, Localization::instance().get("ui.city.best_yield", "BEST")); + } + + // Active indicator or yield amount + painter.setFont(QFont("sans-serif", 8, QFont::Bold)); + painter.setPen(isActive ? QColor(80, 255, 120) : (yieldVal > 0 ? QColor(160, 230, 255) : QColor(130, 145, 160))); + QString yieldText = QString("+%L1").arg(yieldVal); + painter.drawText(QRect(rowRect.right() - 70, rowRect.top(), 64, rowRect.height()), Qt::AlignRight | Qt::AlignVCenter, yieldText); + + currY += rowH; + } } void QSceneRenderer::mousePressEvent(QMouseEvent *event) @@ -1720,7 +1921,55 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event) if (GameManager::instance().isInCityView()) { City *city = GameManager::instance().getActiveCityView(); + // 0a. Handle Resource Selection Menu clicks (if open) + if (m_resourceMenuOpen) { + if (m_resourceMenuCloseBtnRect.contains(event->pos())) { + m_resourceMenuOpen = false; + update(); + return; + } + + for (const auto &choice : m_cityResourceChoiceHits) { + if (choice.rect.contains(event->pos())) { + if (city) { + int tx = m_resourceMenuTx; + int ty = m_resourceMenuTy; + city->setTilePreferredResource(tx, ty, choice.res); + + QVector2D w = Pathfinder::hexToWorld(tx, ty); + float gz = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05); + QString resName = Localization::instance().get(QString("ui.resources.%1").arg(choice.res), choice.res); + + if (city->isTileWorked(tx, ty)) { + // Already working: switch active producing resource + QString msg = Localization::instance().get("ui.city.resource_switched", "Producing %1 (+%2)").arg(resName).arg(choice.amount); + GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(80, 255, 120)); + } else { + if (city->getAvailablePop() > 0) { + city->assignWorker(tx, ty, choice.res); + QString msg = Localization::instance().get("ui.city.worker_assigned_res", "+1 Pop: %1 (+%2)").arg(resName).arg(choice.amount); + GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(80, 255, 120)); + } else { + QString msg = Localization::instance().get("ui.city.resource_preferred", "Slot set to %1 (+%2)").arg(resName).arg(choice.amount); + GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 220, 80)); + } + } + } + m_resourceMenuOpen = false; + update(); + return; + } + } + + // Clicked outside resource menu: dismiss popup + m_resourceMenuOpen = false; + update(); + return; + } + if (m_closeCityViewBtnRect.contains(event->pos())) { + m_resourceMenuOpen = false; + m_slotPressPending = false; cameraPosition = m_savedCamPos; m_zoom = m_savedZoom; zRot = m_savedZRot; @@ -1759,25 +2008,18 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event) } } + // Slot pressed: initiate long press timer for hold-to-change resource for (const auto &slot : m_citySlotHits) { if (slot.rect.contains(event->pos())) { if (city) { - QVector2D w = Pathfinder::hexToWorld(slot.tx, slot.ty); - float gz = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05); - if (city->isTileWorked(slot.tx, slot.ty)) { - city->unassignWorker(slot.tx, slot.ty); - QString msg = Localization::instance().get("ui.city.worker_freed", "Pop worker freed"); - GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 220, 80)); - } else { - if (city->getAvailablePop() > 0) { - city->assignWorker(slot.tx, slot.ty, slot.defaultRes); - QString msg = Localization::instance().get("ui.city.worker_assigned", "+1 Pop working"); - GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(80, 255, 120)); - } else { - QString msg = Localization::instance().get("ui.city.no_free_pop", "No free Pop!"); - GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 80, 80)); - } - } + m_slotPressPending = true; + m_slotLongPressTriggered = false; + m_pressedSlotTx = slot.tx; + m_pressedSlotTy = slot.ty; + m_slotPressPos = event->pos(); + m_slotPressElapsed.start(); + int dur = TerrainYieldManager::instance().getCityConfig().slotLongPressDurationMs; + m_slotLongPressTimer->start(std::max(100, dur)); update(); return; } @@ -1978,6 +2220,14 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event) // Disable panning, rotation and minimap drag in City View if (GameManager::instance().isInCityView()) { + if (m_slotPressPending) { + if ((event->pos() - m_slotPressPos).manhattanLength() > 8) { + if (m_slotLongPressTimer && m_slotLongPressTimer->isActive()) { + m_slotLongPressTimer->stop(); + } + m_slotPressPending = false; + } + } update(); return; } @@ -2005,6 +2255,42 @@ void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event); m_draggingMinimap = false; + + if (m_slotLongPressTimer && m_slotLongPressTimer->isActive()) { + m_slotLongPressTimer->stop(); + } + + if (m_slotPressPending && !m_slotLongPressTriggered) { + // Short click on slot! Toggle worker assignment. + if (GameManager::instance().isInCityView()) { + City *city = GameManager::instance().getActiveCityView(); + if (city) { + int tx = m_pressedSlotTx; + int ty = m_pressedSlotTy; + QVector2D w = Pathfinder::hexToWorld(tx, ty); + float gz = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05); + if (city->isTileWorked(tx, ty)) { + city->unassignWorker(tx, ty); + QString msg = Localization::instance().get("ui.city.worker_freed", "Pop worker freed"); + GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 220, 80)); + } else { + if (city->getAvailablePop() > 0) { + QString bestRes = GameManager::instance().getBestTileResource(tx, ty, m_point, m_numPoints05); + QString res = city->getEffectiveResource(tx, ty, bestRes); + city->assignWorker(tx, ty, res); + QString msg = Localization::instance().get("ui.city.worker_assigned", "+1 Pop working"); + GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(80, 255, 120)); + } else { + QString msg = Localization::instance().get("ui.city.no_free_pop", "No free Pop!"); + GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 80, 80)); + } + } + update(); + } + } + } + m_slotPressPending = false; + m_slotLongPressTriggered = false; } void QSceneRenderer::keyPressEvent(QKeyEvent *e) @@ -2012,6 +2298,12 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e) // In City View: disable WASD panning and Q/E rotation; only allow Escape to exit if (GameManager::instance().isInCityView()) { if (e->key() == Qt::Key_Escape) { + if (m_resourceMenuOpen) { + m_resourceMenuOpen = false; + update(); + return; + } + m_slotPressPending = false; cameraPosition = m_savedCamPos; m_zoom = m_savedZoom; zRot = m_savedZRot; diff --git a/qscenerenderer.h b/qscenerenderer.h index 4b01bbd..7512ece 100644 --- a/qscenerenderer.h +++ b/qscenerenderer.h @@ -146,6 +146,31 @@ protected: int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units QRect m_tabBuildingsRect; QRect m_tabUnitsRect; + + void drawCityResourceMenu(QPainter &painter, const QMatrix4x4 &vMatrix); + void onSlotLongPressTimeout(); + + // Slot Long Press & Resource Selection Menu State + bool m_slotPressPending = false; + bool m_slotLongPressTriggered = false; + int m_pressedSlotTx = -1; + int m_pressedSlotTy = -1; + QPoint m_slotPressPos; + QElapsedTimer m_slotPressElapsed; + QTimer *m_slotLongPressTimer = nullptr; + + bool m_resourceMenuOpen = false; + int m_resourceMenuTx = -1; + int m_resourceMenuTy = -1; + QRect m_resourceMenuRect; + QRect m_resourceMenuCloseBtnRect; + + struct CityResourceChoiceHit { + QRect rect; + QString res; + int amount; + }; + std::vector m_cityResourceChoiceHits; }; #endif // QSCENERENDERER_H diff --git a/terrainyieldmanager.cpp b/terrainyieldmanager.cpp index 8ee0fa1..2ab4677 100644 --- a/terrainyieldmanager.cpp +++ b/terrainyieldmanager.cpp @@ -49,6 +49,12 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir) m_cityConfig.initialNationalResources[it.key()] = it.value().asInt(0); } } + + m_cityConfig.slotLongPressDurationMs = cityRoot["slot_long_press_duration_ms"].asInt(400); + + if (cityRoot.hasChild("resource_priority") && !cityRoot["resource_priority"].listValues.isEmpty()) { + m_cityConfig.resourcePriority = cityRoot["resource_priority"].listValues; + } } // 2. Load terrain_yields.yaml diff --git a/terrainyieldmanager.h b/terrainyieldmanager.h index ad4539e..0437af6 100644 --- a/terrainyieldmanager.h +++ b/terrainyieldmanager.h @@ -35,6 +35,9 @@ struct CityConfig { int tileCultureCost; float foodToInhabitantsRatio; + int slotLongPressDurationMs; + QStringList resourcePriority; + QMap baseYields; QMap initialNationalResources; @@ -43,7 +46,9 @@ struct CityConfig { initialBorderRadius(1), maxWorkableRadius(3), defaultTileSlots(1), cityViewZoom(4.8f), baseResourceYieldPerTick(500), tileCultureCost(10000), - foodToInhabitantsRatio(1.0f) + foodToInhabitantsRatio(1.0f), + slotLongPressDurationMs(400), + resourcePriority({"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"}) { baseYields["voedsel"] = 500; baseYields["arbeid"] = 500;