Merge branch 'main' into session/builder-roads
This commit is contained in:
@@ -59,6 +59,7 @@ public:
|
||||
|
||||
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> 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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
+19
-1
@@ -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!"
|
||||
@@ -119,7 +124,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"
|
||||
@@ -147,3 +152,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"
|
||||
|
||||
+67
-25
@@ -76,10 +76,8 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *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);
|
||||
@@ -251,6 +249,67 @@ void GameManager::exitCityView()
|
||||
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 summary;
|
||||
@@ -270,20 +329,7 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
|
||||
int ty = pair.first.second;
|
||||
QString assignedRes = pair.second;
|
||||
|
||||
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, 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);
|
||||
}
|
||||
}
|
||||
int amount = getTileYield(tx, ty, assignedRes, mapPoints, numPoints05);
|
||||
|
||||
if (assignedRes == "voedsel") summary.food += amount;
|
||||
else if (assignedRes == "arbeid") summary.labor += amount;
|
||||
@@ -636,10 +682,8 @@ void GameManager::commandBuildCitySelected(Matrix2D<TerrainPoint> *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);
|
||||
@@ -1084,9 +1128,7 @@ void GameManager::processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *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);
|
||||
|
||||
@@ -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"
|
||||
@@ -48,6 +49,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;
|
||||
@@ -100,6 +110,10 @@ public:
|
||||
CityYieldSummary calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
|
||||
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 buyTileForCity(int cityId, int tx, int ty);
|
||||
|
||||
|
||||
+316
-24
@@ -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()
|
||||
@@ -1354,15 +1360,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));
|
||||
@@ -1373,6 +1374,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);
|
||||
@@ -1876,6 +1895,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)
|
||||
@@ -1884,7 +2085,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;
|
||||
@@ -1923,25 +2172,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;
|
||||
}
|
||||
@@ -2144,6 +2386,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;
|
||||
}
|
||||
@@ -2171,6 +2421,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)
|
||||
@@ -2178,6 +2464,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;
|
||||
|
||||
@@ -147,6 +147,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<CityResourceChoiceHit> m_cityResourceChoiceHits;
|
||||
};
|
||||
|
||||
#endif // QSCENERENDERER_H
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,6 +35,9 @@ struct CityConfig {
|
||||
int tileCultureCost;
|
||||
float foodToInhabitantsRatio;
|
||||
|
||||
int slotLongPressDurationMs;
|
||||
QStringList resourcePriority;
|
||||
|
||||
QMap<QString, int> baseYields;
|
||||
QMap<QString, int> 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;
|
||||
|
||||
Reference in New Issue
Block a user