Implement default best-yield resource and long-press resource selection for city slots

This commit is contained in:
bart
2026-11-08 21:43:11 +01:00
parent 4b237872eb
commit 43b8b1556d
9 changed files with 492 additions and 52 deletions
+17
View File
@@ -59,6 +59,7 @@ public:
std::vector<std::pair<int, int>> territory; // Owned hex coordinates std::vector<std::pair<int, int>> territory; // Owned hex coordinates
std::map<std::pair<int, int>, QString> workedTiles; // (x, y) -> resource name std::map<std::pair<int, int>, QString> workedTiles; // (x, y) -> resource name
std::map<std::pair<int, int>, QString> tilePreferredResources; // (x, y) -> user chosen resource
City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos, City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos,
int initInhabitants = 1000, int initInhabitantsPerPop = 100000, int initRadius = 1); int initInhabitants = 1000, int initInhabitantsPerPop = 100000, int initRadius = 1);
@@ -127,8 +128,24 @@ public:
return it != workedTiles.end() ? it->second : ""; 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) { bool assignWorker(int tx, int ty, const QString &resource) {
if (!ownsTile(tx, ty)) return false; if (!ownsTile(tx, ty)) return false;
tilePreferredResources[{tx, ty}] = resource;
if (isTileWorked(tx, ty)) { if (isTileWorked(tx, ty)) {
workedTiles[{tx, ty}] = resource; workedTiles[{tx, ty}] = resource;
return true; return true;
+12
View File
@@ -30,3 +30,15 @@ initial_national_resources:
kennis: 0 kennis: 0
geloof: 0 geloof: 0
goud: 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"
+25 -1
View File
@@ -55,6 +55,11 @@ ui:
farm_completed: "Farm completed!" farm_completed: "Farm completed!"
farm_completed_consumed: "Farm completed! (Builder consumed)" farm_completed_consumed: "Farm completed! (Builder consumed)"
farm_completed_charges: "Farm completed! (%1 %2 remaining)" 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_queued: "Found City queued on arrival"
city_founded: "%1 founded!" city_founded: "%1 founded!"
already_building: "Already busy building!" already_building: "Already busy building!"
@@ -88,12 +93,18 @@ ui:
btn_build_farm: "Farm [F]" btn_build_farm: "Farm [F]"
btn_farm_queued: "Farm (Queued)" btn_farm_queued: "Farm (Queued)"
btn_farm_building: "Building %1%" 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_move: "Move (M)"
tip_attack: "Attack (A) [Range: %1]" tip_attack: "Attack (A) [Range: %1]"
tip_build_city: "Found City (B)" tip_build_city: "Found City (B)"
tip_build_farm: "Build Farm (F)" tip_build_farm: "Build Farm (F)"
tip_farm_building: "Busy building... %1%" tip_farm_building: "Busy building... %1%"
tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)" 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: city:
center: "City Center" center: "City Center"
@@ -104,7 +115,7 @@ ui:
close_btn: "[X] Close" close_btn: "[X] Close"
local_info: "Local: Population: %L1 (%2 Pop: %3 working, %4 free) | Labor: %L5" local_info: "Local: Population: %L1 (%2 Pop: %3 working, %4 free) | Labor: %L5"
national_info: "National: Culture: %L1 | Science: %L2 | Gold: %L3 | Faith: %L4" 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" current_production: "CURRENT PRODUCTION"
no_project: "No active project" no_project: "No active project"
select_prompt: "Select a unit or building to construct" select_prompt: "Select a unit or building to construct"
@@ -132,3 +143,16 @@ ui:
no_free_pop: "No free Pop!" no_free_pop: "No free Pop!"
buy_tile: "BUY" buy_tile: "BUY"
occupied: "OCCUPIED" 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"
+67 -25
View File
@@ -72,10 +72,8 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
City initialCity(cId, 1, "Roma", 35, 17, QVector3D(w.x(), w.y(), gz), City initialCity(cId, 1, "Roma", 35, 17, QVector3D(w.x(), w.y(), gz),
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius); cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
// Assign initial 1 pop to work the city center tile // Assign initial 1 pop to work the city center tile with best yield resource
QString tType = TerrainYieldManager::instance().getTerrainType(35, 17, mapPoints, numPoints05); QString defRes = getBestTileResource(35, 17, mapPoints, numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
QString defRes = yCfg ? yCfg->primaryResource : "voedsel";
initialCity.assignWorker(35, 17, defRes); initialCity.assignWorker(35, 17, defRes);
m_cities.push_back(initialCity); m_cities.push_back(initialCity);
@@ -247,6 +245,67 @@ void GameManager::exitCityView()
m_activeCityViewId = -1; 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 = {"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 = {"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 GameManager::calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
{ {
CityYieldSummary summary; CityYieldSummary summary;
@@ -266,20 +325,7 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
int ty = pair.first.second; int ty = pair.first.second;
QString assignedRes = pair.second; QString assignedRes = pair.second;
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); int amount = getTileYield(tx, ty, assignedRes, mapPoints, numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
if (!yCfg) continue;
int amount = yCfg->getYield(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);
}
}
if (assignedRes == "voedsel") summary.food += amount; if (assignedRes == "voedsel") summary.food += amount;
else if (assignedRes == "arbeid") summary.labor += amount; else if (assignedRes == "arbeid") summary.labor += amount;
@@ -632,10 +678,8 @@ void GameManager::commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, in
QVector3D(w.x(), w.y(), gz), QVector3D(w.x(), w.y(), gz),
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius); cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
// Assign initial worker to center tile // Assign initial worker to center tile with best yield resource
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); QString defRes = getBestTileResource(tx, ty, mapPoints, numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
QString defRes = yCfg ? yCfg->primaryResource : "voedsel";
newCity.assignWorker(tx, ty, defRes); newCity.assignWorker(tx, ty, defRes);
m_cities.push_back(newCity); m_cities.push_back(newCity);
@@ -907,9 +951,7 @@ void GameManager::processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *map
QVector3D(w.x(), w.y(), gz), QVector3D(w.x(), w.y(), gz),
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius); cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); QString defRes = getBestTileResource(tx, ty, mapPoints, numPoints05);
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
QString defRes = yCfg ? yCfg->primaryResource : "voedsel";
newCity.assignWorker(tx, ty, defRes); newCity.assignWorker(tx, ty, defRes);
m_cities.push_back(newCity); m_cities.push_back(newCity);
+18 -1
View File
@@ -7,6 +7,7 @@
#include "player.h" #include "player.h"
#include "unitconfig.h" #include "unitconfig.h"
#include "buildingconfig.h" #include "buildingconfig.h"
#include "roadconfig.h"
#include "unit.h" #include "unit.h"
#include "city.h" #include "city.h"
#include "matrix2d.h" #include "matrix2d.h"
@@ -18,7 +19,8 @@ enum class ActionMode {
Move, Move,
Attack, Attack,
BuildCity, BuildCity,
BuildFarm BuildFarm,
BuildRoad
}; };
struct TileImprovement { struct TileImprovement {
@@ -29,6 +31,15 @@ struct TileImprovement {
QVector3D worldPos; 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 { struct FloatingText {
QString text; QString text;
QVector3D worldPos; QVector3D worldPos;
@@ -48,6 +59,8 @@ public:
UnitConfigManager& unitConfigs() { return m_unitConfigManager; } UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; } BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; }
const BuildingConfigManager& buildingConfigs() const { 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<std::shared_ptr<Unit>>& getUnits() const { return m_units; } const std::vector<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
const std::vector<City>& getCities() const { return m_cities; } const std::vector<City>& getCities() const { return m_cities; }
@@ -79,6 +92,10 @@ public:
CityYieldSummary calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const; CityYieldSummary calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
void processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPoints05); void processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
QMap<QString, int> getTileYields(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
int getTileYield(int tx, int ty, const QString &res, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
QString getBestTileResource(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
bool isTileOwned(int tx, int ty, int *outOwnerCityId = nullptr) const; bool isTileOwned(int tx, int ty, int *outOwnerCityId = nullptr) const;
bool buyTileForCity(int cityId, int tx, int ty); bool buyTileForCity(int cityId, int tx, int ty);
+316 -24
View File
@@ -72,6 +72,12 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
QTimer *animTimer = new QTimer(this); QTimer *animTimer = new QTimer(this);
connect(animTimer, &QTimer::timeout, this, QOverload<>::of(&QSceneRenderer::update)); connect(animTimer, &QTimer::timeout, this, QOverload<>::of(&QSceneRenderer::update));
animTimer->start(16); animTimer->start(16);
m_slotLongPressTimer = new QTimer(this);
m_slotLongPressTimer->setSingleShot(true);
connect(m_slotLongPressTimer, &QTimer::timeout, this, [this]() {
onSlotLongPressTimeout();
});
} }
void QSceneRenderer::loadCameraConfig() void QSceneRenderer::loadCameraConfig()
@@ -1190,15 +1196,10 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
if (yCfg && yCfg->workable) { if (yCfg && yCfg->workable) {
// Tile has 1 slot! // Tile has 1 slot!
bool isWorked = city->isTileWorked(hx, hy); bool isWorked = city->isTileWorked(hx, hy);
QString res = isWorked ? city->getWorkedResource(hx, hy) : yCfg->primaryResource; QString bestRes = GameManager::instance().getBestTileResource(hx, hy, m_point, m_numPoints05);
int amount = yCfg->getYield(res); 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); 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; float slotR = 15.0f;
QRect slotRect((int)(sCenter.x() - slotR), (int)(sCenter.y() - slotR), (int)(slotR * 2), (int)(slotR * 2)); 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.setPen(QPen(isWorked ? QColor(80, 240, 120) : QColor(140, 175, 215), isWorked ? 2.2 : 1.2));
painter.drawEllipse(sCenter, slotR, slotR); 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 // Resource icon
QImage rIcon = getTintedIcon("images/resources/" + res + ".png", isWorked ? QColor(80, 255, 120) : QColor(180, 210, 245)); 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); 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++; 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) void QSceneRenderer::mousePressEvent(QMouseEvent *event)
@@ -1720,7 +1921,55 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
if (GameManager::instance().isInCityView()) { if (GameManager::instance().isInCityView()) {
City *city = GameManager::instance().getActiveCityView(); 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())) { if (m_closeCityViewBtnRect.contains(event->pos())) {
m_resourceMenuOpen = false;
m_slotPressPending = false;
cameraPosition = m_savedCamPos; cameraPosition = m_savedCamPos;
m_zoom = m_savedZoom; m_zoom = m_savedZoom;
zRot = m_savedZRot; 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) { for (const auto &slot : m_citySlotHits) {
if (slot.rect.contains(event->pos())) { if (slot.rect.contains(event->pos())) {
if (city) { if (city) {
QVector2D w = Pathfinder::hexToWorld(slot.tx, slot.ty); m_slotPressPending = true;
float gz = Unit::getTerrainHeight(w.x(), w.y(), m_point, m_numPoints05); m_slotLongPressTriggered = false;
if (city->isTileWorked(slot.tx, slot.ty)) { m_pressedSlotTx = slot.tx;
city->unassignWorker(slot.tx, slot.ty); m_pressedSlotTy = slot.ty;
QString msg = Localization::instance().get("ui.city.worker_freed", "Pop worker freed"); m_slotPressPos = event->pos();
GameManager::instance().addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 0.8f), QColor(255, 220, 80)); m_slotPressElapsed.start();
} else { int dur = TerrainYieldManager::instance().getCityConfig().slotLongPressDurationMs;
if (city->getAvailablePop() > 0) { m_slotLongPressTimer->start(std::max(100, dur));
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));
}
}
update(); update();
return; return;
} }
@@ -1978,6 +2220,14 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
// Disable panning, rotation and minimap drag in City View // Disable panning, rotation and minimap drag in City View
if (GameManager::instance().isInCityView()) { 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(); update();
return; return;
} }
@@ -2005,6 +2255,42 @@ void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event)
{ {
Q_UNUSED(event); Q_UNUSED(event);
m_draggingMinimap = false; 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) 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 // In City View: disable WASD panning and Q/E rotation; only allow Escape to exit
if (GameManager::instance().isInCityView()) { if (GameManager::instance().isInCityView()) {
if (e->key() == Qt::Key_Escape) { if (e->key() == Qt::Key_Escape) {
if (m_resourceMenuOpen) {
m_resourceMenuOpen = false;
update();
return;
}
m_slotPressPending = false;
cameraPosition = m_savedCamPos; cameraPosition = m_savedCamPos;
m_zoom = m_savedZoom; m_zoom = m_savedZoom;
zRot = m_savedZRot; zRot = m_savedZRot;
+25
View File
@@ -146,6 +146,31 @@ protected:
int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units
QRect m_tabBuildingsRect; QRect m_tabBuildingsRect;
QRect m_tabUnitsRect; 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<CityResourceChoiceHit> m_cityResourceChoiceHits;
}; };
#endif // QSCENERENDERER_H #endif // QSCENERENDERER_H
+6
View File
@@ -49,6 +49,12 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir)
m_cityConfig.initialNationalResources[it.key()] = it.value().asInt(0); 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 // 2. Load terrain_yields.yaml
+6 -1
View File
@@ -35,6 +35,9 @@ struct CityConfig {
int tileCultureCost; int tileCultureCost;
float foodToInhabitantsRatio; float foodToInhabitantsRatio;
int slotLongPressDurationMs;
QStringList resourcePriority;
QMap<QString, int> baseYields; QMap<QString, int> baseYields;
QMap<QString, int> initialNationalResources; QMap<QString, int> initialNationalResources;
@@ -43,7 +46,9 @@ struct CityConfig {
initialBorderRadius(1), maxWorkableRadius(3), defaultTileSlots(1), initialBorderRadius(1), maxWorkableRadius(3), defaultTileSlots(1),
cityViewZoom(4.8f), cityViewZoom(4.8f),
baseResourceYieldPerTick(500), tileCultureCost(10000), baseResourceYieldPerTick(500), tileCultureCost(10000),
foodToInhabitantsRatio(1.0f) foodToInhabitantsRatio(1.0f),
slotLongPressDurationMs(400),
resourcePriority({"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"})
{ {
baseYields["voedsel"] = 500; baseYields["voedsel"] = 500;
baseYields["arbeid"] = 500; baseYields["arbeid"] = 500;