Refactor farm building speed to builder unit action_speeds and enable infinite charges
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Tile improvements configuration
|
||||
# All values must be configurable via YAML as per AGENTS.md
|
||||
|
||||
farm:
|
||||
id: farm
|
||||
name: "Boerderij"
|
||||
action: "boerderij_bouwen"
|
||||
texture: "images/farm.jpeg"
|
||||
texture_scale: 0.6 # World-space scale: 0.6 spans nicely across adjacent hexes without repeating unnaturally
|
||||
texture_mapping: "world" # "world" = seamless across adjacent hex tiles; "hex" = centered per tile
|
||||
valid_terrains:
|
||||
- grasland
|
||||
- tundra
|
||||
yield_bonus:
|
||||
voedsel: 1000
|
||||
@@ -0,0 +1,15 @@
|
||||
id: builder
|
||||
name: Builder
|
||||
category: bouwen
|
||||
domain: land
|
||||
health: 100
|
||||
movement_speed: 2.0
|
||||
cost: 50
|
||||
build_charges: -1 # -1 voor oneindig doorwerken
|
||||
infinite_charges: true
|
||||
icon: images/units/builder.png
|
||||
actions:
|
||||
- verplaatsen
|
||||
- boerderij_bouwen
|
||||
action_speeds:
|
||||
boerderij_bouwen: 10.0 # Duur in seconden die deze builder unit nodig heeft voor de actie boerderij_bouwen
|
||||
+241
-3
@@ -2,6 +2,8 @@
|
||||
#include "pathfinder.h"
|
||||
#include "yamlparser.h"
|
||||
#include "terrainyieldmanager.h"
|
||||
#include "terrainmovementmanager.h"
|
||||
#include "localization.h"
|
||||
#include <QDebug>
|
||||
#include <cmath>
|
||||
|
||||
@@ -23,6 +25,9 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
||||
{
|
||||
if (m_initialized) return;
|
||||
|
||||
// Load localization
|
||||
Localization::instance().loadFile(baseDir + "/config/strings_en.yaml");
|
||||
|
||||
// Load terrain yields & city configuration
|
||||
TerrainYieldManager::instance().loadConfigs(baseDir);
|
||||
|
||||
@@ -37,8 +42,17 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
||||
QString unitsConfigDir = baseDir + "/config/units";
|
||||
m_unitConfigManager.loadFromDirectory(unitsConfigDir);
|
||||
|
||||
// Spawn an initial established city for Player 1 at (35, 17)
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
|
||||
// Initialize player national resources from configuration
|
||||
for (auto &player : m_playerManager.getPlayersMutable()) {
|
||||
player.culture = cCfg.initialNationalResources.value("cultuur", 10000);
|
||||
player.science = cCfg.initialNationalResources.value("kennis", 0);
|
||||
player.faith = cCfg.initialNationalResources.value("geloof", 0);
|
||||
player.gold = cCfg.initialNationalResources.value("goud", 0);
|
||||
}
|
||||
|
||||
// Spawn an initial established city for Player 1 at (35, 17)
|
||||
int cId = getNextCityId();
|
||||
QVector2D w = Pathfinder::hexToWorld(35, 17);
|
||||
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
|
||||
@@ -57,8 +71,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
||||
// Spawn initial demo units near starting camera position
|
||||
// Player 1 (Blue & Gold)
|
||||
spawnUnit("settler", 1, 36, 18, mapPoints, numPoints05);
|
||||
spawnUnit("builder", 1, 35, 18, mapPoints, numPoints05);
|
||||
spawnUnit("warrior", 1, 37, 18, mapPoints, numPoints05);
|
||||
spawnUnit("slinger", 1, 37, 19, mapPoints, numPoints05);
|
||||
spawnUnit("galley", 1, 41, 92, mapPoints, numPoints05);
|
||||
|
||||
// Player 2 (Red & White) - Opponent units
|
||||
spawnUnit("warrior", 2, 40, 18, mapPoints, numPoints05);
|
||||
@@ -92,12 +108,16 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
|
||||
m_tickTimer += deltaTime * m_simSpeedMultiplier;
|
||||
if (m_tickTimer >= m_movementTickDuration) {
|
||||
m_tickTimer = std::fmod(m_tickTimer, m_movementTickDuration);
|
||||
processTickYields(mapPoints, numPoints05);
|
||||
}
|
||||
|
||||
// Update units
|
||||
for (auto &u : m_units) {
|
||||
if (!u->isDead()) {
|
||||
u->update(deltaTime, m_movementTickDuration, m_simSpeedMultiplier, mapPoints, numPoints05);
|
||||
if (u->isBuilding() && u->buildProgress >= 1.0f) {
|
||||
completeUnitBuilding(*u, mapPoints, numPoints05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +234,17 @@ void GameManager::exitCityView()
|
||||
CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
|
||||
{
|
||||
CityYieldSummary summary;
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
|
||||
// Base yields for every city per tick
|
||||
summary.food += cCfg.getBaseYield("voedsel");
|
||||
summary.labor += cCfg.getBaseYield("arbeid");
|
||||
summary.science += cCfg.getBaseYield("kennis");
|
||||
summary.culture += cCfg.getBaseYield("cultuur");
|
||||
summary.faith += cCfg.getBaseYield("geloof");
|
||||
summary.gold += cCfg.getBaseYield("goud");
|
||||
|
||||
// Additional yields from worked tiles
|
||||
for (const auto &pair : city.workedTiles) {
|
||||
int tx = pair.first.first;
|
||||
int ty = pair.first.second;
|
||||
@@ -224,6 +255,16 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
|
||||
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;
|
||||
else if (assignedRes == "arbeid") summary.labor += amount;
|
||||
else if (assignedRes == "kennis") summary.science += amount;
|
||||
@@ -234,6 +275,88 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
|
||||
return summary;
|
||||
}
|
||||
|
||||
void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig();
|
||||
|
||||
for (auto &city : m_cities) {
|
||||
CityYieldSummary yields = calculateCityYields(city, mapPoints, numPoints05);
|
||||
|
||||
// 1. Food is converted into inhabitants:
|
||||
int foodGrown = (int)std::round(yields.food * cfg.foodToInhabitantsRatio);
|
||||
city.inhabitants += foodGrown;
|
||||
|
||||
// 2. Labor is accumulated locally in the city:
|
||||
city.accumulatedLabor += yields.labor;
|
||||
|
||||
// 3. National resources go to the owner player:
|
||||
Player *owner = m_playerManager.getPlayer(city.playerId);
|
||||
if (owner) {
|
||||
owner->culture += yields.culture;
|
||||
owner->science += yields.science;
|
||||
owner->faith += yields.faith;
|
||||
owner->gold += yields.gold;
|
||||
}
|
||||
|
||||
// Floating growth text over city
|
||||
QVector3D ftPos = city.worldPos + QVector3D(0.0f, 0.0f, 1.1f);
|
||||
addFloatingText(QString("+%1 Inwoners | +%2 Cultuur").arg(foodGrown).arg(yields.culture),
|
||||
ftPos, QColor(100, 255, 140), 2.5f);
|
||||
}
|
||||
}
|
||||
|
||||
bool GameManager::isTileOwned(int tx, int ty, int *outOwnerCityId) const
|
||||
{
|
||||
for (const auto &c : m_cities) {
|
||||
if (c.ownsTile(tx, ty)) {
|
||||
if (outOwnerCityId) *outOwnerCityId = c.id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GameManager::buyTileForCity(int cityId, int tx, int ty)
|
||||
{
|
||||
City *city = getCityById(cityId);
|
||||
if (!city) return false;
|
||||
|
||||
// Check if tile is already owned by any city
|
||||
int existingOwnerId = -1;
|
||||
if (isTileOwned(tx, ty, &existingOwnerId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig();
|
||||
|
||||
// Check distance (must be within max workable radius)
|
||||
int dist = Pathfinder::hexDistance(city->tileX, city->tileY, tx, ty);
|
||||
if (dist > cfg.maxWorkableRadius) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check culture cost
|
||||
Player *player = m_playerManager.getPlayer(city->playerId);
|
||||
if (!player) return false;
|
||||
|
||||
int cost = cfg.tileCultureCost;
|
||||
if (player->culture < cost) {
|
||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||
addFloatingText(QString("Niet genoeg Cultuur! (%1 nodig)").arg(cost),
|
||||
QVector3D(w.x(), w.y(), 0.8f), QColor(255, 90, 90), 2.0f);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deduct culture and claim tile
|
||||
player->culture -= cost;
|
||||
city->addTerritory(tx, ty);
|
||||
|
||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||
addFloatingText(QString("Tegel gekocht! (-%1 Cultuur)").arg(cost),
|
||||
QVector3D(w.x(), w.y(), 0.8f), QColor(210, 120, 255), 2.5f);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
Unit *selected = getSelectedUnit();
|
||||
@@ -243,13 +366,31 @@ void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<Terrain
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target tile is passable for this unit's movement domain
|
||||
if (!TerrainMovementManager::instance().isHexPassable(
|
||||
targetX, targetY, selected->config.movementDomain, mapPoints, numPoints05, &selected->config)) {
|
||||
if (selected->isNaval()) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.naval_only_water", "Ships can only sail on water!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
||||
} else {
|
||||
addFloatingText(Localization::instance().get("ui.unit.impassable_terrain", "Impassable terrain!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
|
||||
selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05
|
||||
selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05,
|
||||
selected->config.movementDomain, &selected->config
|
||||
);
|
||||
|
||||
if (path.size() > 1) {
|
||||
selected->setPath(path);
|
||||
addFloatingText("Verplaatsen", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
|
||||
addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
|
||||
} else {
|
||||
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,3 +459,100 @@ void GameManager::addFloatingText(const QString &text, const QVector3D &worldPos
|
||||
{
|
||||
m_floatingTexts.push_back({text, worldPos, duration, duration, color});
|
||||
}
|
||||
|
||||
const TileImprovement* GameManager::getImprovementAt(int tx, int ty) const
|
||||
{
|
||||
auto it = m_tileImprovements.find({tx, ty});
|
||||
if (it != m_tileImprovements.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool GameManager::hasImprovementAt(int tx, int ty) const
|
||||
{
|
||||
return m_tileImprovements.find({tx, ty}) != m_tileImprovements.end();
|
||||
}
|
||||
|
||||
void GameManager::commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
Unit *selected = getSelectedUnit();
|
||||
if (!selected || !selected->config.canBuildFarm()) return;
|
||||
|
||||
if (selected->isBuilding()) {
|
||||
addFloatingText("Al bezig met bouwen!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 200, 70));
|
||||
return;
|
||||
}
|
||||
|
||||
int tx = selected->tileX;
|
||||
int ty = selected->tileY;
|
||||
|
||||
// Check if tile already has an improvement
|
||||
if (hasImprovementAt(tx, ty)) {
|
||||
addFloatingText("Er staat hier al een boerderij!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check terrain type against allowed terrains loaded from improvements.yaml
|
||||
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
|
||||
const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig("farm");
|
||||
QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"};
|
||||
|
||||
if (!validTerrains.contains(tType)) {
|
||||
addFloatingText("Kan hier niet bouwen! Alleen op gras en toendra.", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
// Start building process (duration comes from the builder unit's action speed)
|
||||
float duration = selected->config.getActionDuration("boerderij_bouwen", 10.0f);
|
||||
selected->startBuilding("farm", duration);
|
||||
addFloatingText("Boerderij in aanbouw...", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
|
||||
}
|
||||
|
||||
void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
int tx = unit.tileX;
|
||||
int ty = unit.tileY;
|
||||
QString impId = unit.buildingImprovementId.isEmpty() ? "farm" : unit.buildingImprovementId;
|
||||
|
||||
if (hasImprovementAt(tx, ty)) {
|
||||
unit.cancelBuilding();
|
||||
return;
|
||||
}
|
||||
|
||||
// Place improvement now that it is completed!
|
||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
|
||||
|
||||
TileImprovement imp;
|
||||
imp.improvementId = impId;
|
||||
imp.tileX = tx;
|
||||
imp.tileY = ty;
|
||||
imp.playerId = unit.playerId;
|
||||
imp.worldPos = QVector3D(w.x(), w.y(), gz);
|
||||
m_tileImprovements[{tx, ty}] = imp;
|
||||
|
||||
unit.cancelBuilding();
|
||||
|
||||
if (!unit.config.infiniteCharges && unit.buildCharges > 0) {
|
||||
unit.buildCharges -= 1;
|
||||
if (unit.buildCharges <= 0) {
|
||||
addFloatingText("Boerderij voltooid! (Builder verbruikt)", QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
|
||||
unit.takeDamage(99999.0f);
|
||||
if (unit.id == m_selectedUnitId) {
|
||||
m_selectedUnitId = -1;
|
||||
m_actionMode = ActionMode::None;
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
QString msg = QString("Boerderij voltooid! (%1 %2 over)")
|
||||
.arg(unit.buildCharges)
|
||||
.arg(unit.buildCharges == 1 ? "lading" : "ladingen");
|
||||
addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
addFloatingText("Boerderij voltooid!", QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
|
||||
}
|
||||
|
||||
|
||||
+367
-79
@@ -6,6 +6,7 @@
|
||||
#include <QToolTip>
|
||||
#include <QDebug>
|
||||
#include "pathfinder.h"
|
||||
#include "yamlparser.h"
|
||||
|
||||
GLuint vertexbuffer;
|
||||
GLuint VertexArrayID;
|
||||
@@ -46,6 +47,7 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
|
||||
}
|
||||
}
|
||||
|
||||
loadCameraConfig();
|
||||
zRot = 90.0f; // Default looking North (+Y)
|
||||
m_zoom = 8.0f;
|
||||
cameraUpDirection = cameraTransformation * QVector3D(0, 1, 0);
|
||||
@@ -72,6 +74,46 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
|
||||
animTimer->start(16);
|
||||
}
|
||||
|
||||
void QSceneRenderer::loadCameraConfig()
|
||||
{
|
||||
QString configPath = "/home/bart/project/tiletest/config/game.yaml";
|
||||
YamlNode root = YamlParser::parseFile(configPath);
|
||||
if (root.hasChild("camera")) {
|
||||
YamlNode cam = root["camera"];
|
||||
m_cameraMinZoom = cam["min_zoom"].asFloat(m_cameraMinZoom);
|
||||
m_cameraMaxZoom = cam["max_zoom"].asFloat(m_cameraMaxZoom);
|
||||
m_cameraPitchMinDeg = cam["pitch_min_deg"].asFloat(m_cameraPitchMinDeg);
|
||||
m_cameraPitchMaxDeg = cam["pitch_max_deg"].asFloat(m_cameraPitchMaxDeg);
|
||||
}
|
||||
}
|
||||
|
||||
float QSceneRenderer::getCameraPitchDeg() const
|
||||
{
|
||||
float range = m_cameraMaxZoom - m_cameraMinZoom;
|
||||
float zoomT = range > 1e-4f ? std::max(0.0f, std::min(1.0f, (m_zoom - m_cameraMinZoom) / range)) : 0.0f;
|
||||
return m_cameraPitchMinDeg + zoomT * (m_cameraPitchMaxDeg - m_cameraPitchMinDeg);
|
||||
}
|
||||
|
||||
float QSceneRenderer::getCameraForwardDist() const
|
||||
{
|
||||
float pitchRad = getCameraPitchDeg() * PI / 180.0f;
|
||||
float tanPitch = std::tan(pitchRad);
|
||||
if (tanPitch < 1e-4f) tanPitch = 1e-4f;
|
||||
return m_zoom / tanPitch;
|
||||
}
|
||||
|
||||
QVector3D QSceneRenderer::getCameraLookAt() const
|
||||
{
|
||||
float forwardDist = getCameraForwardDist();
|
||||
float rad = zRot * PI / 180.0f;
|
||||
return QVector3D(
|
||||
cameraPosition.x() + forwardDist * cos(rad),
|
||||
cameraPosition.y() + forwardDist * sin(rad),
|
||||
0.0f
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void QSceneRenderer::loadRiverMap(const QImage &riverImg)
|
||||
{
|
||||
if (riverImg.isNull()) return;
|
||||
@@ -323,7 +365,9 @@ void QSceneRenderer::drawMinimap(QPainter &painter)
|
||||
|
||||
float rad = zRot * PI / 180.0f;
|
||||
float fovHalf = 25.0f * PI / 180.0f;
|
||||
float viewDist = std::max(12.0f, m_zoom * 2.2f);
|
||||
float pitchDeg = getCameraPitchDeg();
|
||||
float farPitchRad = (pitchDeg - 22.5f) * PI / 180.0f;
|
||||
float viewDist = std::max(4.0f, (float)(m_zoom / std::tan(farPitchRad)));
|
||||
|
||||
float lx = cameraPosition.x() + viewDist * cos(rad - fovHalf);
|
||||
float ly = cameraPosition.y() + viewDist * sin(rad - fovHalf);
|
||||
@@ -374,10 +418,11 @@ void QSceneRenderer::drawMinimap(QPainter &painter)
|
||||
QRect bottomBar(cardRect.left() + 6, mapRect.bottom() + 4, cardRect.width() - 12, 16);
|
||||
painter.setFont(QFont("sans-serif", 8));
|
||||
painter.setPen(QColor(180, 200, 220));
|
||||
QString info = QString("Cam: %1, %2 km | Alt: %3 km")
|
||||
QString info = QString("Cam: %1, %2 km | Alt: %3 km | Hoek: %4°")
|
||||
.arg(cameraPosition.x(), 0, 'f', 1)
|
||||
.arg(cameraPosition.y(), 0, 'f', 1)
|
||||
.arg(m_zoom, 0, 'f', 1);
|
||||
.arg(m_zoom, 0, 'f', 1)
|
||||
.arg(pitchDeg, 0, 'f', 0);
|
||||
painter.drawText(bottomBar, Qt::AlignLeft | Qt::AlignVCenter, info);
|
||||
}
|
||||
|
||||
@@ -452,15 +497,15 @@ void QSceneRenderer::drawTickBar(QPainter &painter)
|
||||
{
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
int barW = 340, barH = 34;
|
||||
int barW = 690, barH = 34;
|
||||
QRect barRect(16, 16, barW, barH);
|
||||
|
||||
// Dark glass card
|
||||
painter.setBrush(QColor(18, 24, 34, 230));
|
||||
painter.setBrush(QColor(18, 24, 34, 235));
|
||||
painter.setPen(QPen(QColor(70, 100, 140, 180), 1.2));
|
||||
painter.drawRoundedRect(barRect, 6, 6);
|
||||
|
||||
// Movement Tick Info
|
||||
// 1. Movement Tick Info
|
||||
float tickDur = GameManager::instance().getTickDuration();
|
||||
float curTick = GameManager::instance().getTickTimer();
|
||||
float progress = curTick / tickDur;
|
||||
@@ -471,16 +516,40 @@ void QSceneRenderer::drawTickBar(QPainter &painter)
|
||||
painter.drawText(QRect(barRect.left() + 10, barRect.top() + 4, 120, 14), Qt::AlignLeft | Qt::AlignVCenter, tickLabel);
|
||||
|
||||
// Progress bar
|
||||
QRect progBg(barRect.left() + 10, barRect.top() + 20, 140, 6);
|
||||
QRect progBg(barRect.left() + 10, barRect.top() + 20, 130, 6);
|
||||
painter.setBrush(QColor(30, 40, 55));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRoundedRect(progBg, 3, 3);
|
||||
|
||||
QRect progFg(barRect.left() + 10, barRect.top() + 20, (int)(140 * progress), 6);
|
||||
QRect progFg(barRect.left() + 10, barRect.top() + 20, (int)(130 * progress), 6);
|
||||
painter.setBrush(QColor(60, 180, 255));
|
||||
painter.drawRoundedRect(progFg, 3, 3);
|
||||
|
||||
// Sim Speed Buttons [1x] [2x] [5x]
|
||||
// 2. Player 1 National Resources (Empire-wide)
|
||||
const Player *p1 = GameManager::instance().players().getPlayer(1);
|
||||
if (p1) {
|
||||
struct ResItem { QString icon; int val; QColor col; };
|
||||
std::vector<ResItem> natRes = {
|
||||
{"images/resources/cultuur.png", p1->culture, QColor(215, 110, 255)},
|
||||
{"images/resources/kennis.png", p1->science, QColor(60, 190, 255)},
|
||||
{"images/resources/goud.png", p1->gold, QColor(255, 210, 50)},
|
||||
{"images/resources/geloof.png", p1->faith, QColor(255, 230, 80)}
|
||||
};
|
||||
|
||||
int startX = barRect.left() + 155;
|
||||
int itemW = 86;
|
||||
for (size_t i = 0; i < natRes.size(); ++i) {
|
||||
int ix = startX + (int)i * itemW;
|
||||
QImage icon = getTintedIcon(natRes[i].icon, natRes[i].col);
|
||||
painter.drawImage(QRect(ix, barRect.top() + 9, 16, 16), icon);
|
||||
|
||||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||||
painter.setPen(natRes[i].col);
|
||||
painter.drawText(QRect(ix + 20, barRect.top() + 8, itemW - 22, 18), Qt::AlignLeft | Qt::AlignVCenter, QString("%L1").arg(natRes[i].val));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Sim Speed Buttons [1x] [2x] [5x]
|
||||
float curSpeed = GameManager::instance().getSimSpeed();
|
||||
m_speed1xRect = QRect(barRect.right() - 150, barRect.top() + 6, 42, 22);
|
||||
m_speed2xRect = QRect(barRect.right() - 102, barRect.top() + 6, 42, 22);
|
||||
@@ -534,9 +603,18 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
|
||||
|
||||
// 2. Draw Units: Teardrop (Druppel in secondary color) + Icon (in primary color)
|
||||
const float R = 14.0f; // Head circle radius
|
||||
City *activeCityView = GameManager::instance().getActiveCityView();
|
||||
int maxCityRadius = TerrainYieldManager::instance().getCityConfig().maxWorkableRadius;
|
||||
|
||||
for (const auto &unit : GameManager::instance().getUnits()) {
|
||||
if (unit->isDead()) continue;
|
||||
|
||||
if (activeCityView) {
|
||||
if (Pathfinder::hexDistance(activeCityView->tileX, activeCityView->tileY, unit->tileX, unit->tileY) > maxCityRadius) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool inFront = false;
|
||||
QVector3D topCylinder = unit->worldPos + QVector3D(0.0f, 0.0f, 0.38f);
|
||||
QPointF tipPos = UnitRenderer::projectToScreen(topCylinder, Projection, vMatrix, width(), height(), inFront);
|
||||
@@ -589,6 +667,19 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
|
||||
painter.setBrush(hpFrac > 0.5f ? QColor(70, 220, 90) : (hpFrac > 0.25f ? QColor(255, 190, 40) : QColor(240, 50, 50)));
|
||||
painter.drawRoundedRect(hpFg, 1.5, 1.5);
|
||||
|
||||
// Building progress bar if unit is constructing
|
||||
if (unit->isBuilding()) {
|
||||
QRectF bpBg(headCenter.x() - 14.0f, headCenter.y() - R - 1.5f, 28.0f, 3.5f);
|
||||
painter.setBrush(QColor(30, 30, 30, 220));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRoundedRect(bpBg, 1.5, 1.5);
|
||||
|
||||
float bpFrac = std::max(0.0f, std::min(1.0f, unit->buildProgress));
|
||||
QRectF bpFg(headCenter.x() - 14.0f, headCenter.y() - R - 1.5f, 28.0f * bpFrac, 3.5f);
|
||||
painter.setBrush(QColor(255, 185, 45));
|
||||
painter.drawRoundedRect(bpFg, 1.5, 1.5);
|
||||
}
|
||||
|
||||
// If unit is selected: pulsing selection halo & floating action bubbles
|
||||
if (unit.get() == selectedUnit) {
|
||||
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.008f) + 1.0f) * 0.5f;
|
||||
@@ -624,6 +715,15 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
|
||||
isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity);
|
||||
iconFile = "images/actions/build_city.png";
|
||||
tip = "Steden bouwen (B)";
|
||||
} else if (act == "boerderij_bouwen" || act == "build_farm") {
|
||||
isActive = unit->isBuilding();
|
||||
iconFile = "images/actions/farm.png";
|
||||
if (unit->isBuilding()) {
|
||||
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
|
||||
tip = QString("Aan het bouwen... %1%").arg(pct);
|
||||
} else {
|
||||
tip = "Boerderij bouwen (F)";
|
||||
}
|
||||
}
|
||||
|
||||
// Draw action bubble
|
||||
@@ -822,6 +922,17 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
|
||||
if (unit->config.isMilitary()) {
|
||||
QString combatInfo = QString("Aanval: %1 | Bereik: %2 hex").arg(unit->config.attackDamage).arg(unit->config.attackRange);
|
||||
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, combatInfo);
|
||||
} else if (unit->isBuilding()) {
|
||||
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
|
||||
QString buildInfo = QString("Aan het bouwen... %1%").arg(pct);
|
||||
if (!unit->config.infiniteCharges && unit->buildCharges > 0) {
|
||||
buildInfo += QString(" | Ladingen: %1").arg(unit->buildCharges);
|
||||
}
|
||||
painter.setPen(QColor(255, 210, 80));
|
||||
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, buildInfo);
|
||||
} else if (unit->config.canBuildFarm() && !unit->config.infiniteCharges && unit->buildCharges > 0) {
|
||||
QString builderInfo = QString("Civiele eenheid | Ladingen: %1").arg(unit->buildCharges);
|
||||
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, builderInfo);
|
||||
} else {
|
||||
painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, "Civiele eenheid");
|
||||
}
|
||||
@@ -860,6 +971,17 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
|
||||
iconFile = "images/actions/build_city.png";
|
||||
label = "Sticht [B]";
|
||||
tip = "Steden bouwen (B)";
|
||||
} else if (act == "boerderij_bouwen" || act == "build_farm") {
|
||||
isActive = unit->isBuilding();
|
||||
iconFile = "images/actions/farm.png";
|
||||
if (unit->isBuilding()) {
|
||||
int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f);
|
||||
label = QString("Bouwt %1%").arg(pct);
|
||||
tip = QString("Bezig met bouwen... %1%").arg(pct);
|
||||
} else {
|
||||
label = "Boerderij [F]";
|
||||
tip = "Boerderij bouwen (F)";
|
||||
}
|
||||
}
|
||||
|
||||
// Circular bubble button
|
||||
@@ -883,8 +1005,11 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit)
|
||||
void QSceneRenderer::drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix)
|
||||
{
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
City *activeCity = GameManager::instance().getActiveCityView();
|
||||
|
||||
for (const auto &city : GameManager::instance().getCities()) {
|
||||
if (activeCity && city.id != activeCity->id) continue;
|
||||
|
||||
const Player *player = GameManager::instance().players().getPlayer(city.playerId);
|
||||
QColor ribbonColor = player ? player->primaryColor : QColor(255, 215, 60);
|
||||
|
||||
@@ -928,9 +1053,47 @@ void QSceneRenderer::drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatri
|
||||
}
|
||||
}
|
||||
|
||||
void QSceneRenderer::drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
|
||||
{
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
const auto &improvements = GameManager::instance().getTileImprovements();
|
||||
if (improvements.empty()) return;
|
||||
|
||||
for (const auto &pair : improvements) {
|
||||
const TileImprovement &imp = pair.second;
|
||||
bool inFront = false;
|
||||
QPointF screenP = UnitRenderer::projectToScreen(imp.worldPos + QVector3D(0, 0, 0.20f), Projection, vMatrix, width(), height(), inFront);
|
||||
if (!inFront) continue;
|
||||
|
||||
// Draw styled badge for improvement (Farm)
|
||||
float r = 14.0f;
|
||||
QRect badgeRect((int)(screenP.x() - r), (int)(screenP.y() - r), (int)(r * 2), (int)(r * 2));
|
||||
|
||||
// Background disc
|
||||
painter.setBrush(QColor(24, 30, 20, 235));
|
||||
painter.setPen(QPen(QColor(235, 195, 60), 1.6));
|
||||
painter.drawEllipse(badgeRect);
|
||||
|
||||
// Icon inside disc
|
||||
QImage farmIcon = getTintedIcon("images/actions/farm.png", QColor(255, 220, 75));
|
||||
painter.drawImage(QRectF(screenP.x() - 8, screenP.y() - 8, 16, 16), farmIcon);
|
||||
|
||||
// Label pill under badge
|
||||
QRect labelRect((int)screenP.x() - 26, (int)screenP.y() + 13, 52, 13);
|
||||
painter.setBrush(QColor(15, 20, 28, 220));
|
||||
painter.setPen(QPen(QColor(200, 165, 50, 150), 1.0));
|
||||
painter.drawRoundedRect(labelRect, 3, 3);
|
||||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||||
painter.setPen(QColor(255, 230, 130));
|
||||
painter.drawText(labelRect, Qt::AlignCenter, "Boerderij");
|
||||
}
|
||||
}
|
||||
|
||||
void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix)
|
||||
{
|
||||
m_citySlotHits.clear();
|
||||
m_cityBuyTileHits.clear();
|
||||
City *city = GameManager::instance().getActiveCityView();
|
||||
if (!city) return;
|
||||
|
||||
@@ -943,8 +1106,8 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
|
||||
int maxRadius = TerrainYieldManager::instance().getCityConfig().maxWorkableRadius; // 3 deep hexagons
|
||||
|
||||
// 1. Render all hexes up to 3 distance around the city
|
||||
for (int dx = -maxRadius - 1; dx <= maxRadius + 1; ++dx) {
|
||||
for (int dy = -maxRadius - 1; dy <= maxRadius + 1; ++dy) {
|
||||
for (int dx = -maxRadius - 2; dx <= maxRadius + 2; ++dx) {
|
||||
for (int dy = -maxRadius - 2; dy <= maxRadius + 2; ++dy) {
|
||||
int hx = city->tileX + dx;
|
||||
int hy = city->tileY + dy;
|
||||
if (hx < 0 || hx >= MAP_WIDTH || hy < 0 || hy >= MAP_HEIGHT) continue;
|
||||
@@ -996,6 +1159,13 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
|
||||
bool isWorked = city->isTileWorked(hx, hy);
|
||||
QString res = isWorked ? city->getWorkedResource(hx, hy) : yCfg->primaryResource;
|
||||
int amount = yCfg->getYield(res);
|
||||
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));
|
||||
@@ -1010,6 +1180,12 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
|
||||
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);
|
||||
|
||||
// If tile has farm/improvement, draw small icon on upper right of slot
|
||||
if (imp) {
|
||||
QImage fIcon = getTintedIcon("images/actions/farm.png", QColor(255, 215, 60));
|
||||
painter.drawImage(QRectF(sCenter.x() + 8, sCenter.y() - 16, 14, 14), fIcon);
|
||||
}
|
||||
|
||||
// Yield pill below slot
|
||||
QRect pillRect((int)sCenter.x() - 26, (int)sCenter.y() + 16, 52, 14);
|
||||
painter.setBrush(QColor(15, 20, 28, 220));
|
||||
@@ -1029,33 +1205,64 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
|
||||
}
|
||||
// If not workable (e.g. Sneeuw / Snow): no slot is shown, as requested!
|
||||
} else {
|
||||
// Outside borders: draw subtle locked badge
|
||||
QRect lockRect((int)sCenter.x() - 12, (int)sCenter.y() - 7, 24, 14);
|
||||
painter.setBrush(QColor(20, 24, 32, 180));
|
||||
painter.setPen(QPen(QColor(100, 120, 140, 100), 1.0));
|
||||
painter.drawRoundedRect(lockRect, 3, 3);
|
||||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||||
painter.setPen(QColor(140, 155, 175));
|
||||
painter.drawText(lockRect, Qt::AlignCenter, "SLOT");
|
||||
int otherCityId = -1;
|
||||
bool isOwnedByOther = GameManager::instance().isTileOwned(hx, hy, &otherCityId);
|
||||
if (!isOwnedByOther) {
|
||||
// Unowned tile: Can be bought with culture!
|
||||
int cultureCost = TerrainYieldManager::instance().getCityConfig().tileCultureCost;
|
||||
bool canAfford = (player && player->culture >= cultureCost);
|
||||
|
||||
QRect buyRect((int)sCenter.x() - 34, (int)sCenter.y() - 14, 68, 28);
|
||||
m_cityBuyTileHits.push_back({buyRect, hx, hy});
|
||||
|
||||
// Background & border
|
||||
painter.setBrush(canAfford ? QColor(40, 20, 55, 235) : QColor(24, 18, 30, 215));
|
||||
painter.setPen(QPen(canAfford ? QColor(220, 110, 255) : QColor(120, 80, 140), canAfford ? 1.8 : 1.0));
|
||||
painter.drawRoundedRect(buyRect, 5, 5);
|
||||
|
||||
// Culture icon
|
||||
QImage cultIcon = getTintedIcon("images/resources/cultuur.png", canAfford ? QColor(225, 120, 255) : QColor(150, 110, 170));
|
||||
painter.drawImage(QRect(buyRect.left() + 4, buyRect.top() + 6, 16, 16), cultIcon);
|
||||
|
||||
// Price text
|
||||
painter.setFont(QFont("sans-serif", 7, QFont::Bold));
|
||||
painter.setPen(canAfford ? QColor(255, 230, 255) : QColor(165, 145, 180));
|
||||
QString costStr = QString("%L1").arg(cultureCost);
|
||||
painter.drawText(QRect(buyRect.left() + 22, buyRect.top() + 2, buyRect.width() - 24, 13), Qt::AlignLeft | Qt::AlignVCenter, costStr);
|
||||
|
||||
// "Koop" subtitle
|
||||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||||
painter.setPen(canAfford ? QColor(220, 140, 255) : QColor(140, 120, 155));
|
||||
painter.drawText(QRect(buyRect.left() + 22, buyRect.top() + 14, buyRect.width() - 24, 12), Qt::AlignLeft | Qt::AlignVCenter, "KOOP");
|
||||
} else {
|
||||
// Owned by another city
|
||||
QRect lockRect((int)sCenter.x() - 16, (int)sCenter.y() - 8, 32, 16);
|
||||
painter.setBrush(QColor(40, 20, 20, 190));
|
||||
painter.setPen(QPen(QColor(160, 60, 60, 140), 1.0));
|
||||
painter.drawRoundedRect(lockRect, 3, 3);
|
||||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||||
painter.setPen(QColor(230, 130, 130));
|
||||
painter.drawText(lockRect, Qt::AlignCenter, "BEZET");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. City Management Top Card
|
||||
int cardW = 500, cardH = 145;
|
||||
QRect cardRect((width() - cardW) / 2, 16, cardW, cardH);
|
||||
int cardW = 560, cardH = 174;
|
||||
QRect cardRect((width() - cardW) / 2, 14, cardW, cardH);
|
||||
|
||||
painter.setBrush(QColor(16, 22, 32, 242));
|
||||
painter.setBrush(QColor(16, 22, 32, 245));
|
||||
painter.setPen(QPen(primaryCol, 1.8));
|
||||
painter.drawRoundedRect(cardRect, 10, 10);
|
||||
|
||||
// City Title & Close Button
|
||||
painter.setFont(QFont("sans-serif", 13, QFont::Bold));
|
||||
painter.setPen(QColor(245, 250, 255));
|
||||
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 10, 260, 24), Qt::AlignLeft | Qt::AlignVCenter, city->name.toUpper() + " - STADSBEHEER");
|
||||
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 8, 300, 24), Qt::AlignLeft | Qt::AlignVCenter, city->name.toUpper() + " - STADSBEHEER");
|
||||
|
||||
// Close button [X]
|
||||
m_closeCityViewBtnRect = QRect(cardRect.right() - 88, cardRect.top() + 10, 74, 24);
|
||||
m_closeCityViewBtnRect = QRect(cardRect.right() - 88, cardRect.top() + 8, 74, 24);
|
||||
painter.setBrush(QColor(40, 50, 68, 220));
|
||||
painter.setPen(QPen(secondaryCol, 1.2));
|
||||
painter.drawRoundedRect(m_closeCityViewBtnRect, 4, 4);
|
||||
@@ -1063,22 +1270,34 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
|
||||
painter.setPen(QColor(220, 235, 255));
|
||||
painter.drawText(m_closeCityViewBtnRect, Qt::AlignCenter, "[X] Sluiten");
|
||||
|
||||
// Population Stats Row
|
||||
// Row 1: Population & Local Resources (Food & Labor are local to the city)
|
||||
int totalPop = city->getTotalPop();
|
||||
int assigned = city->getAssignedPop();
|
||||
int avail = city->getAvailablePop();
|
||||
|
||||
painter.setFont(QFont("sans-serif", 9));
|
||||
painter.setFont(QFont("sans-serif", 8));
|
||||
painter.setPen(QColor(180, 205, 235));
|
||||
QString popInfo = QString("Inwoners: %L1 | Totale Pop: %2 | Aan het werk: %3 | Beschikbaar: %4")
|
||||
.arg(city->inhabitants).arg(totalPop).arg(assigned).arg(avail);
|
||||
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 38, cardRect.width() - 32, 18), Qt::AlignLeft | Qt::AlignVCenter, popInfo);
|
||||
QString popInfo = QString("Lokaal: Inwoners: %L1 (%2 Pop: %3 aan het werk, %4 vrij) | Arbeid: %L5")
|
||||
.arg(city->inhabitants).arg(totalPop).arg(assigned).arg(avail).arg(city->accumulatedLabor);
|
||||
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 34, cardRect.width() - 32, 16), Qt::AlignLeft | Qt::AlignVCenter, popInfo);
|
||||
|
||||
// Row 2: National Resources (Culture, Science, Gold, Faith belong to the nation)
|
||||
int cult = player ? player->culture : 0;
|
||||
int sci = player ? player->science : 0;
|
||||
int gld = player ? player->gold : 0;
|
||||
int fth = player ? player->faith : 0;
|
||||
|
||||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||||
painter.setPen(QColor(220, 160, 255));
|
||||
QString natInfo = QString("Nationaal: Cultuur: %L1 | Kennis: %L2 | Goud: %L3 | Geloof: %L4")
|
||||
.arg(cult).arg(sci).arg(gld).arg(fth);
|
||||
painter.drawText(QRect(cardRect.left() + 16, cardRect.top() + 52, cardRect.width() - 32, 16), Qt::AlignLeft | Qt::AlignVCenter, natInfo);
|
||||
|
||||
// Separator line
|
||||
painter.setPen(QPen(QColor(60, 85, 120, 150), 1.0));
|
||||
painter.drawLine(cardRect.left() + 16, cardRect.top() + 60, cardRect.right() - 16, cardRect.top() + 60);
|
||||
painter.drawLine(cardRect.left() + 16, cardRect.top() + 72, cardRect.right() - 16, cardRect.top() + 72);
|
||||
|
||||
// Total Yields Summary Row
|
||||
// Row 3: Total Yields Summary Row (Base 500 + worked tiles)
|
||||
CityYieldSummary summary = GameManager::instance().calculateCityYields(*city, m_point, m_numPoints05);
|
||||
|
||||
struct YieldItem { QString name; int val; QString icon; QColor col; };
|
||||
@@ -1094,29 +1313,29 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
|
||||
int colW = (cardRect.width() - 32) / 6;
|
||||
for (size_t i = 0; i < yields.size(); ++i) {
|
||||
int cx = cardRect.left() + 16 + (int)i * colW;
|
||||
QRect yRect(cx, cardRect.top() + 68, colW - 6, 44);
|
||||
QRect yRect(cx, cardRect.top() + 78, colW - 6, 42);
|
||||
|
||||
painter.setBrush(QColor(12, 16, 24, 180));
|
||||
painter.setPen(QPen(QColor(40, 60, 85, 120), 1.0));
|
||||
painter.drawRoundedRect(yRect, 4, 4);
|
||||
|
||||
QImage icon = getTintedIcon(yields[i].icon, yields[i].col);
|
||||
painter.drawImage(QRect(yRect.left() + 6, yRect.top() + 6, 16, 16), icon);
|
||||
painter.drawImage(QRect(yRect.left() + 6, yRect.top() + 5, 16, 16), icon);
|
||||
|
||||
painter.setFont(QFont("sans-serif", 7));
|
||||
painter.setPen(QColor(160, 180, 205));
|
||||
painter.drawText(QRect(yRect.left() + 26, yRect.top() + 6, yRect.width() - 28, 16), Qt::AlignLeft | Qt::AlignVCenter, yields[i].name);
|
||||
painter.drawText(QRect(yRect.left() + 26, yRect.top() + 5, yRect.width() - 28, 16), Qt::AlignLeft | Qt::AlignVCenter, yields[i].name);
|
||||
|
||||
painter.setFont(QFont("sans-serif", 9, QFont::Bold));
|
||||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||||
painter.setPen(yields[i].col);
|
||||
painter.drawText(QRect(yRect.left() + 6, yRect.top() + 24, yRect.width() - 12, 16), Qt::AlignLeft | Qt::AlignVCenter, QString("+%1").arg(yields[i].val));
|
||||
painter.drawText(QRect(yRect.left() + 6, yRect.top() + 22, yRect.width() - 12, 16), Qt::AlignLeft | Qt::AlignVCenter, QString("+%1/tick").arg(yields[i].val));
|
||||
}
|
||||
|
||||
// Help instruction at bottom
|
||||
painter.setFont(QFont("sans-serif", 7, QFont::StyleItalic));
|
||||
painter.setPen(QColor(140, 170, 200));
|
||||
painter.drawText(QRect(cardRect.left() + 16, cardRect.bottom() - 20, cardRect.width() - 32, 16), Qt::AlignCenter,
|
||||
"Klik op een tegel binnen de grenzen om 1 Pop toe te wijzen of vrij te maken.");
|
||||
painter.drawText(QRect(cardRect.left() + 16, cardRect.bottom() - 18, cardRect.width() - 32, 16), Qt::AlignCenter,
|
||||
"Klik op een tegel om 1 Pop toe te wijzen | Klik op een vrije tegel om deze te kopen met Cultuur.");
|
||||
}
|
||||
|
||||
void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
||||
@@ -1124,6 +1343,9 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
||||
// 0. If in City View: handle close button and slot clicks
|
||||
if (GameManager::instance().isInCityView()) {
|
||||
if (m_closeCityViewBtnRect.contains(event->pos())) {
|
||||
cameraPosition = m_savedCamPos;
|
||||
m_zoom = m_savedZoom;
|
||||
zRot = m_savedZRot;
|
||||
GameManager::instance().exitCityView();
|
||||
update();
|
||||
return;
|
||||
@@ -1151,17 +1373,28 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Buy tile clicks
|
||||
for (const auto &buyHit : m_cityBuyTileHits) {
|
||||
if (buyHit.rect.contains(event->pos())) {
|
||||
City *city = GameManager::instance().getActiveCityView();
|
||||
if (city) {
|
||||
GameManager::instance().buyTileForCity(city->id, buyHit.tx, buyHit.ty);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return; // In City View, ignore all other clicks
|
||||
}
|
||||
|
||||
// Check if clicked on a City Banner to open City View
|
||||
for (const auto &cb : m_cityBannerHits) {
|
||||
if (cb.rect.contains(event->pos())) {
|
||||
m_savedCamPos = cameraPosition;
|
||||
m_savedZoom = m_zoom;
|
||||
m_savedZRot = zRot;
|
||||
GameManager::instance().enterCityView(cb.cityId);
|
||||
City *c = GameManager::instance().getCityById(cb.cityId);
|
||||
if (c) {
|
||||
cameraPosition.setX(c->worldPos.x());
|
||||
cameraPosition.setY(c->worldPos.y() - 2.8f);
|
||||
}
|
||||
update();
|
||||
return;
|
||||
}
|
||||
@@ -1229,6 +1462,8 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
||||
}
|
||||
} else if (ab.actionId == "steden_bouwen" || ab.actionId == "build_city") {
|
||||
GameManager::instance().commandBuildCitySelected(m_point, m_numPoints05);
|
||||
} else if (ab.actionId == "boerderij_bouwen" || ab.actionId == "build_farm") {
|
||||
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
|
||||
}
|
||||
update();
|
||||
return;
|
||||
@@ -1238,11 +1473,7 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
||||
// Compute view matrix for raycasting and 3D screen tests
|
||||
QMatrix4x4 vMatrix;
|
||||
cameraUpDirection = cameraTransformation * QVector3D(0, 0, 1);
|
||||
QVector3D lookat = QVector3D(
|
||||
cameraPosition.x() + m_zoom * m_zoom / 5.0f * cos(PI * zRot / 180.0f),
|
||||
cameraPosition.y() + m_zoom * m_zoom / 5.0f * sin(PI * zRot / 180.0f),
|
||||
0
|
||||
);
|
||||
QVector3D lookat = getCameraLookAt();
|
||||
vMatrix.lookAt(cameraPosition, lookat, cameraUpDirection);
|
||||
|
||||
// 5. Left Click: Check if directly clicked on any unit's Druppel pin
|
||||
@@ -1304,9 +1535,10 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
||||
} else {
|
||||
City *cityOnTile = GameManager::instance().getCityAt(hitX, hitY);
|
||||
if (cityOnTile) {
|
||||
m_savedCamPos = cameraPosition;
|
||||
m_savedZoom = m_zoom;
|
||||
m_savedZRot = zRot;
|
||||
GameManager::instance().enterCityView(cityOnTile->id);
|
||||
cameraPosition.setX(cityOnTile->worldPos.x());
|
||||
cameraPosition.setY(cityOnTile->worldPos.y() - 2.8f);
|
||||
} else if (!m_unitCardRect.contains(event->pos())) {
|
||||
GameManager::instance().deselectUnit();
|
||||
}
|
||||
@@ -1330,6 +1562,12 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
m_mouseCurrentPos = event->pos();
|
||||
|
||||
// Disable panning, rotation and minimap drag in City View
|
||||
if (GameManager::instance().isInCityView()) {
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
QRect mapRect = getMinimapRect();
|
||||
if (m_draggingMinimap && (event->buttons() & Qt::LeftButton)) {
|
||||
QVector2D wPos = minimapToWorld(event->pos(), mapRect);
|
||||
@@ -1357,6 +1595,18 @@ void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event)
|
||||
|
||||
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) {
|
||||
cameraPosition = m_savedCamPos;
|
||||
m_zoom = m_savedZoom;
|
||||
zRot = m_savedZRot;
|
||||
GameManager::instance().exitCityView();
|
||||
update();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float speed = (e->modifiers() & Qt::ShiftModifier) ? 1.2f : 0.4f;
|
||||
|
||||
// Movement & camera controls
|
||||
@@ -1400,10 +1650,11 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e)
|
||||
else if (e->key() == Qt::Key_B && sel && sel->config.canBuildCity()) {
|
||||
GameManager::instance().commandBuildCitySelected(m_point, m_numPoints05);
|
||||
}
|
||||
else if (e->key() == Qt::Key_F && sel && sel->config.canBuildFarm()) {
|
||||
GameManager::instance().commandBuildFarmSelected(m_point, m_numPoints05);
|
||||
}
|
||||
else if (e->key() == Qt::Key_Escape) {
|
||||
if (GameManager::instance().isInCityView()) {
|
||||
GameManager::instance().exitCityView();
|
||||
} else if (GameManager::instance().currentActionMode() != ActionMode::None) {
|
||||
if (GameManager::instance().currentActionMode() != ActionMode::None) {
|
||||
GameManager::instance().setActionMode(ActionMode::None);
|
||||
} else {
|
||||
GameManager::instance().deselectUnit();
|
||||
@@ -1425,6 +1676,11 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e)
|
||||
|
||||
void QSceneRenderer::wheelEvent(QWheelEvent *e)
|
||||
{
|
||||
// Disable zooming in City View
|
||||
if (GameManager::instance().isInCityView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QRect card = getMinimapCardRect();
|
||||
if (card.contains(e->pos()) || getZoomOutButtonRect().contains(e->pos()) || getZoomInButtonRect().contains(e->pos())) {
|
||||
if (e->delta() > 0) {
|
||||
@@ -1437,8 +1693,8 @@ void QSceneRenderer::wheelEvent(QWheelEvent *e)
|
||||
}
|
||||
|
||||
m_zoom -= e->delta() / 100.0f;
|
||||
if (m_zoom < 2.0f) m_zoom = 2.0f;
|
||||
if (m_zoom > 16.0f) m_zoom = 16.0f;
|
||||
if (m_zoom < m_cameraMinZoom) m_zoom = m_cameraMinZoom;
|
||||
if (m_zoom > m_cameraMaxZoom) m_zoom = m_cameraMaxZoom;
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -1462,17 +1718,38 @@ void QSceneRenderer::paintGL()
|
||||
QMatrix4x4 cameraTrans;
|
||||
QMatrix4x4 vMatrix;
|
||||
|
||||
City *cityView = GameManager::instance().getActiveCityView();
|
||||
int cityViewRadius = cityView ? TerrainYieldManager::instance().getCityConfig().maxWorkableRadius : -1;
|
||||
|
||||
cameraTrans.rotate(zRot, 0, 0, 1);
|
||||
cameraUpDirection = cameraTrans * QVector3D(0, 0, 1);
|
||||
cameraPosition.setZ(m_zoom);
|
||||
|
||||
QVector3D lookat = QVector3D(
|
||||
cameraPosition.x() + m_zoom * m_zoom / 5.0f * cos(PI * zRot / 180.0f),
|
||||
cameraPosition.y() + m_zoom * m_zoom / 5.0f * sin(PI * zRot / 180.0f),
|
||||
0.0f
|
||||
);
|
||||
vMatrix.lookAt(cameraPosition, lookat, cameraUpDirection);
|
||||
QVector3D eyePos;
|
||||
QVector3D lookat;
|
||||
|
||||
if (cityView) {
|
||||
float effectiveZoom = TerrainYieldManager::instance().getCityConfig().cityViewZoom;
|
||||
float pitchDist = effectiveZoom * effectiveZoom / 5.0f;
|
||||
float rad = zRot * PI / 180.0f;
|
||||
lookat = cityView->worldPos;
|
||||
eyePos = QVector3D(
|
||||
lookat.x() - pitchDist * cos(rad),
|
||||
lookat.y() - pitchDist * sin(rad),
|
||||
effectiveZoom
|
||||
);
|
||||
cameraPosition = eyePos;
|
||||
} else {
|
||||
cameraPosition.setZ(m_zoom);
|
||||
eyePos = cameraPosition;
|
||||
lookat = getCameraLookAt();
|
||||
}
|
||||
vMatrix.lookAt(eyePos, lookat, cameraUpDirection);
|
||||
|
||||
if (cityView) {
|
||||
glClearColor(0.06f, 0.08f, 0.12f, 1.0f);
|
||||
} else {
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Update simulation
|
||||
@@ -1482,12 +1759,13 @@ void QSceneRenderer::paintGL()
|
||||
GameManager::instance().update(deltaTime, m_point, m_numPoints05);
|
||||
|
||||
// Visible terrain tiles bounding box
|
||||
float minWorldX = std::min(cameraPosition.x(), lookat.x());
|
||||
float maxWorldX = std::max(cameraPosition.x(), lookat.x());
|
||||
float minWorldY = std::min(cameraPosition.y(), lookat.y());
|
||||
float maxWorldY = std::max(cameraPosition.y(), lookat.y());
|
||||
float minWorldX = std::min(eyePos.x(), lookat.x());
|
||||
float maxWorldX = std::max(eyePos.x(), lookat.x());
|
||||
float minWorldY = std::min(eyePos.y(), lookat.y());
|
||||
float maxWorldY = std::max(eyePos.y(), lookat.y());
|
||||
|
||||
float margin = std::max(m_zoom * 3.0f + 15.0f, 35.0f);
|
||||
// Margin around visible focal area (provides ample buffer for mountains and rotating view)
|
||||
float margin = cityView ? 20.0f : std::max(10.0f, m_zoom * 1.4f + 4.0f);
|
||||
minWorldX -= margin;
|
||||
maxWorldX += margin;
|
||||
minWorldY -= margin;
|
||||
@@ -1518,10 +1796,13 @@ void QSceneRenderer::paintGL()
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Render terrain tiles
|
||||
// 1. Render terrain tiles (in city view: ONLY render tiles within cityViewRadius!)
|
||||
for (int y = minTileY; y <= maxTileY; ++y) {
|
||||
for (int x = minTileX; x <= maxTileX; ++x) {
|
||||
tiles->operator()(x, y).paint(Projection, cameraPosition, cameraUpDirection, vMatrix, currentTime);
|
||||
if (cityView && Pathfinder::hexDistance(cityView->tileX, cityView->tileY, x, y) > cityViewRadius) {
|
||||
continue;
|
||||
}
|
||||
tiles->operator()(x, y).paint(Projection, eyePos, cameraUpDirection, vMatrix, currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1538,22 +1819,29 @@ void QSceneRenderer::paintGL()
|
||||
Projection,
|
||||
vMatrix,
|
||||
vSunPos,
|
||||
selected ? selected->id : -1
|
||||
selected ? selected->id : -1,
|
||||
cityViewRadius,
|
||||
cityView ? cityView->tileX : -1,
|
||||
cityView ? cityView->tileY : -1
|
||||
);
|
||||
|
||||
// 3. Render 2D UI & HUD Overlays via QPainter
|
||||
QPainter painter(this);
|
||||
drawCityBorders(painter, vMatrix);
|
||||
drawPathAndRange(painter, selected, vMatrix);
|
||||
drawMinimap(painter);
|
||||
drawTickBar(painter);
|
||||
drawUnitsOverlay(painter, vMatrix);
|
||||
drawFloatingTexts(painter, vMatrix);
|
||||
|
||||
if (GameManager::instance().isInCityView()) {
|
||||
drawImprovementsOverlay(painter, vMatrix);
|
||||
if (!cityView) {
|
||||
drawPathAndRange(painter, selected, vMatrix);
|
||||
drawMinimap(painter);
|
||||
drawTickBar(painter);
|
||||
drawUnitsOverlay(painter, vMatrix);
|
||||
drawFloatingTexts(painter, vMatrix);
|
||||
if (selected) {
|
||||
drawUnitCard(painter, selected);
|
||||
}
|
||||
} else {
|
||||
drawUnitsOverlay(painter, vMatrix);
|
||||
drawFloatingTexts(painter, vMatrix);
|
||||
drawCityViewOverlay(painter, vMatrix);
|
||||
} else if (selected) {
|
||||
drawUnitCard(painter, selected);
|
||||
}
|
||||
painter.end();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ uniform sampler2D texTundra; // Unit 8 (Texturelabs_Soil_135XL tundra soil)
|
||||
uniform sampler2D texRedSand; // Unit 9 (Texturelabs_RedSand prairie sand)
|
||||
uniform sampler2D texRedRock; // Unit 10 (Texturelabs_RedRock canyon cliff)
|
||||
uniform sampler2D texForest; // Unit 11 (Texturelabs_Forest_seamless woodland floor)
|
||||
uniform sampler2D texFarm; // Unit 12 (images/farm.jpeg)
|
||||
|
||||
uniform int isFarm;
|
||||
uniform float farmTextureScale;
|
||||
uniform int farmMappingMode;
|
||||
|
||||
uniform float time;
|
||||
uniform int riverMask;
|
||||
@@ -213,6 +218,22 @@ void main() {
|
||||
+ tundraColor * tundraWeight
|
||||
+ redSandColor * prairieWeight;
|
||||
|
||||
// Farm improvement texture applied when a farm is present
|
||||
if (isFarm == 1) {
|
||||
float scale = max(0.01, farmTextureScale);
|
||||
vec2 farmUV;
|
||||
if (farmMappingMode == 1) {
|
||||
// Hex-centered mapping with aspect ratio correction
|
||||
vec2 centered = vec2(tilePos.x - 0.5, (tilePos.y - 0.4328799) * 1.154734411);
|
||||
farmUV = centered * scale + vec2(0.5, 0.5);
|
||||
} else {
|
||||
// World-space coordinates: perfectly seamless and continuous across adjacent hex tiles
|
||||
farmUV = vWorldPos.xy * scale;
|
||||
}
|
||||
vec4 farmColor = texture(texFarm, farmUV);
|
||||
biomeGround = farmColor;
|
||||
}
|
||||
|
||||
// Shoreline beach: where gentle slopes meet water (sea level up to ~110m, or along rivers)
|
||||
// Steep slopes (norm.z < 0.55) remain rocky cliffs; gentle slopes (norm.z > 0.80) get a sandy beach
|
||||
float gentleSlope = smoothstep(0.55, 0.85, norm.z);
|
||||
|
||||
+67
-36
@@ -1,4 +1,5 @@
|
||||
#include "terrainyieldmanager.h"
|
||||
#include "terrainmovementmanager.h"
|
||||
#include "yamlparser.h"
|
||||
#include "pathfinder.h"
|
||||
#include <QDebug>
|
||||
@@ -17,6 +18,9 @@ TerrainYieldManager::TerrainYieldManager()
|
||||
|
||||
bool TerrainYieldManager::loadConfigs(const QString &baseDir)
|
||||
{
|
||||
// 0. Load terrain_movement.yaml
|
||||
TerrainMovementManager::instance().loadConfig(baseDir + "/config/terrain_movement.yaml");
|
||||
|
||||
// 1. Load city.yaml
|
||||
QString cityFile = baseDir + "/config/city.yaml";
|
||||
YamlNode cityRoot = YamlParser::parseFile(cityFile);
|
||||
@@ -26,6 +30,25 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir)
|
||||
m_cityConfig.initialBorderRadius = cityRoot["initial_border_radius"].asInt(1);
|
||||
m_cityConfig.maxWorkableRadius = cityRoot["max_workable_radius"].asInt(3);
|
||||
m_cityConfig.defaultTileSlots = cityRoot["default_tile_slots"].asInt(1);
|
||||
m_cityConfig.cityViewZoom = cityRoot["city_view_zoom"].asFloat(4.8f);
|
||||
|
||||
m_cityConfig.baseResourceYieldPerTick = cityRoot["base_resource_yield_per_tick"].asInt(500);
|
||||
m_cityConfig.tileCultureCost = cityRoot["tile_culture_cost"].asInt(10000);
|
||||
m_cityConfig.foodToInhabitantsRatio = cityRoot["food_to_inhabitants_ratio"].asFloat(1.0f);
|
||||
|
||||
if (cityRoot.hasChild("base_yields")) {
|
||||
const YamlNode &bNode = cityRoot["base_yields"];
|
||||
for (auto it = bNode.children.begin(); it != bNode.children.end(); ++it) {
|
||||
m_cityConfig.baseYields[it.key()] = it.value().asInt(m_cityConfig.baseResourceYieldPerTick);
|
||||
}
|
||||
}
|
||||
|
||||
if (cityRoot.hasChild("initial_national_resources")) {
|
||||
const YamlNode &rNode = cityRoot["initial_national_resources"];
|
||||
for (auto it = rNode.children.begin(); it != rNode.children.end(); ++it) {
|
||||
m_cityConfig.initialNationalResources[it.key()] = it.value().asInt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Load terrain_yields.yaml
|
||||
@@ -53,48 +76,46 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir)
|
||||
m_yieldConfigs[typeKey] = cfg;
|
||||
}
|
||||
|
||||
// 3. Load improvements.yaml
|
||||
QString impFile = baseDir + "/config/improvements.yaml";
|
||||
YamlNode impRoot = YamlParser::parseFile(impFile);
|
||||
for (auto it = impRoot.children.begin(); it != impRoot.children.end(); ++it) {
|
||||
QString impKey = it.key();
|
||||
const YamlNode &node = it.value();
|
||||
|
||||
ImprovementConfig cfg;
|
||||
cfg.id = node.hasChild("id") ? node["id"].asString() : impKey;
|
||||
cfg.name = node.hasChild("name") ? node["name"].asString() : cfg.id;
|
||||
cfg.action = node.hasChild("action") ? node["action"].asString() : "boerderij_bouwen";
|
||||
cfg.texturePath = node.hasChild("texture") ? node["texture"].asString() : "images/farm.jpeg";
|
||||
cfg.textureScale = node.hasChild("texture_scale") ? node["texture_scale"].asFloat(0.6f) : 0.6f;
|
||||
cfg.textureMapping = node.hasChild("texture_mapping") ? node["texture_mapping"].asString("world") : "world";
|
||||
|
||||
if (node.hasChild("valid_terrains")) {
|
||||
cfg.validTerrains = node["valid_terrains"].listValues;
|
||||
} else {
|
||||
cfg.validTerrains = QStringList{"grasland", "tundra"};
|
||||
}
|
||||
|
||||
if (node.hasChild("yield_bonus")) {
|
||||
const YamlNode &ybNode = node["yield_bonus"];
|
||||
for (auto ybIt = ybNode.children.begin(); ybIt != ybNode.children.end(); ++ybIt) {
|
||||
cfg.yieldBonus[ybIt.key()] = ybIt.value().asInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
m_improvementConfigs[impKey] = cfg;
|
||||
}
|
||||
|
||||
m_loaded = true;
|
||||
qDebug() << "TerrainYieldManager loaded" << m_yieldConfigs.size() << "terrain types and city config.";
|
||||
qDebug() << "TerrainYieldManager loaded" << m_yieldConfigs.size() << "terrain types,"
|
||||
<< m_improvementConfigs.size() << "improvements, and city config.";
|
||||
return true;
|
||||
}
|
||||
|
||||
QString TerrainYieldManager::getTerrainType(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
|
||||
{
|
||||
if (!mapPoints || numPoints05 <= 0) return "grasland";
|
||||
|
||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||
float scaleX = numPoints05 * 2.0f;
|
||||
float scaleY = numPoints05 / LOD_0_SIZE_Y;
|
||||
|
||||
int mapX = (int) std::round(w.x() * scaleX);
|
||||
int mapY = (int) std::round(w.y() * scaleY);
|
||||
|
||||
if (mapX < 0) mapX = 0;
|
||||
if (mapX >= (int)mapPoints->getWidth()) mapX = (int)mapPoints->getWidth() - 1;
|
||||
if (mapY < 0) mapY = 0;
|
||||
if (mapY >= (int)mapPoints->getHeight()) mapY = (int)mapPoints->getHeight() - 1;
|
||||
|
||||
TerrainPoint tp = mapPoints->operator()(mapX, mapY);
|
||||
|
||||
// Sea level water
|
||||
if (tp.height < 6) return "water";
|
||||
|
||||
float heightKm = (tp.height / 255.0f) * 3.5f + 1.0f;
|
||||
|
||||
// High peaks have snow
|
||||
if (heightKm >= 3.1f) return "sneeuw";
|
||||
|
||||
// Mountain rock
|
||||
if (heightKm >= 2.3f) return "steen";
|
||||
|
||||
switch (tp.type) {
|
||||
case 0: return "grasland";
|
||||
case 1: return "sahara";
|
||||
case 2: return "tundra";
|
||||
case 3: return "red_canyon";
|
||||
case 4: return "bos";
|
||||
default: return "grasland";
|
||||
}
|
||||
return TerrainMovementManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
|
||||
}
|
||||
|
||||
const TerrainYieldConfig* TerrainYieldManager::getYieldConfig(const QString &terrainType) const
|
||||
@@ -105,3 +126,13 @@ const TerrainYieldConfig* TerrainYieldManager::getYieldConfig(const QString &ter
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ImprovementConfig* TerrainYieldManager::getImprovementConfig(const QString &id) const
|
||||
{
|
||||
auto it = m_improvementConfigs.find(id);
|
||||
if (it != m_improvementConfigs.end()) {
|
||||
return &it.value();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
+47
-1
@@ -29,10 +29,52 @@ struct CityConfig {
|
||||
int initialBorderRadius;
|
||||
int maxWorkableRadius;
|
||||
int defaultTileSlots;
|
||||
float cityViewZoom;
|
||||
|
||||
int baseResourceYieldPerTick;
|
||||
int tileCultureCost;
|
||||
float foodToInhabitantsRatio;
|
||||
|
||||
QMap<QString, int> baseYields;
|
||||
QMap<QString, int> initialNationalResources;
|
||||
|
||||
CityConfig()
|
||||
: initialInhabitants(1000), inhabitantsPerPop(100000),
|
||||
initialBorderRadius(1), maxWorkableRadius(3), defaultTileSlots(1) {}
|
||||
initialBorderRadius(1), maxWorkableRadius(3), defaultTileSlots(1),
|
||||
cityViewZoom(4.8f),
|
||||
baseResourceYieldPerTick(500), tileCultureCost(10000),
|
||||
foodToInhabitantsRatio(1.0f)
|
||||
{
|
||||
baseYields["voedsel"] = 500;
|
||||
baseYields["arbeid"] = 500;
|
||||
baseYields["kennis"] = 500;
|
||||
baseYields["cultuur"] = 500;
|
||||
baseYields["geloof"] = 500;
|
||||
baseYields["goud"] = 500;
|
||||
|
||||
initialNationalResources["cultuur"] = 10000;
|
||||
initialNationalResources["kennis"] = 0;
|
||||
initialNationalResources["geloof"] = 0;
|
||||
initialNationalResources["goud"] = 0;
|
||||
}
|
||||
|
||||
int getBaseYield(const QString &res) const {
|
||||
return baseYields.value(res, baseResourceYieldPerTick);
|
||||
}
|
||||
};
|
||||
|
||||
struct ImprovementConfig {
|
||||
QString id;
|
||||
QString name;
|
||||
QString action;
|
||||
QString texturePath;
|
||||
float textureScale;
|
||||
QString textureMapping; // "world" or "hex"
|
||||
QStringList validTerrains;
|
||||
QMap<QString, int> yieldBonus; // e.g. "voedsel": 1000
|
||||
|
||||
ImprovementConfig()
|
||||
: id(""), name(""), action(""), texturePath("images/farm.jpeg"), textureScale(0.6f), textureMapping("world") {}
|
||||
};
|
||||
|
||||
class TerrainYieldManager {
|
||||
@@ -45,11 +87,15 @@ public:
|
||||
const TerrainYieldConfig* getYieldConfig(const QString &terrainType) const;
|
||||
const CityConfig& getCityConfig() const { return m_cityConfig; }
|
||||
|
||||
const ImprovementConfig* getImprovementConfig(const QString &id) const;
|
||||
const QMap<QString, ImprovementConfig>& getAllImprovementConfigs() const { return m_improvementConfigs; }
|
||||
|
||||
private:
|
||||
TerrainYieldManager();
|
||||
~TerrainYieldManager() = default;
|
||||
|
||||
QMap<QString, TerrainYieldConfig> m_yieldConfigs;
|
||||
QMap<QString, ImprovementConfig> m_improvementConfigs;
|
||||
CityConfig m_cityConfig;
|
||||
bool m_loaded;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "matrix2d.h"
|
||||
#include <math.h>
|
||||
#include "qscenerenderer.h"
|
||||
#include "gamemanager.h"
|
||||
#include <QVector3D>
|
||||
QImage * Tile::m_image_grass = NULL;
|
||||
QOpenGLTexture * Tile::m_texture_grass = NULL;
|
||||
@@ -27,6 +28,8 @@ QOpenGLTexture * Tile::m_texture_redrock = NULL;
|
||||
QImage * Tile::m_image_redrock = NULL;
|
||||
QOpenGLTexture * Tile::m_texture_forest = NULL;
|
||||
QImage * Tile::m_image_forest = NULL;
|
||||
QOpenGLTexture * Tile::m_texture_farm = NULL;
|
||||
QImage * Tile::m_image_farm = NULL;
|
||||
|
||||
Vertex * Tile::m_vertices= NULL;
|
||||
int Tile::m_numVertex;
|
||||
@@ -147,6 +150,8 @@ void Tile::init(QSceneRenderer* s, int x, int y)
|
||||
Tile::m_image_redrock = new QImage("/home/bart/project/tiletest/images/Texturelabs_RedRock_seamless.jpg");
|
||||
if (Tile::m_image_forest == NULL)
|
||||
Tile::m_image_forest = new QImage("/home/bart/project/tiletest/images/Texturelabs_Forest_seamless.jpg");
|
||||
if (Tile::m_image_farm == NULL)
|
||||
Tile::m_image_farm = new QImage("/home/bart/project/tiletest/images/farm.jpeg");
|
||||
|
||||
if (Tile::m_texture_grass == NULL) {
|
||||
m_texture_grass = new QOpenGLTexture(*m_image_grass);
|
||||
@@ -232,6 +237,13 @@ void Tile::init(QSceneRenderer* s, int x, int y)
|
||||
m_texture_forest->setMagnificationFilter(QOpenGLTexture::Linear);
|
||||
m_texture_forest->generateMipMaps();
|
||||
}
|
||||
if (Tile::m_texture_farm == NULL && Tile::m_image_farm != NULL) {
|
||||
m_texture_farm = new QOpenGLTexture(*m_image_farm);
|
||||
m_texture_farm->setWrapMode(QOpenGLTexture::Repeat);
|
||||
m_texture_farm->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear);
|
||||
m_texture_farm->setMagnificationFilter(QOpenGLTexture::Linear);
|
||||
m_texture_farm->generateMipMaps();
|
||||
}
|
||||
|
||||
if (first){
|
||||
m_program = new QOpenGLShaderProgram();
|
||||
@@ -302,6 +314,9 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
|
||||
m_texture_redsand->bind(9);
|
||||
m_texture_redrock->bind(10);
|
||||
m_texture_forest->bind(11);
|
||||
if (m_texture_farm) {
|
||||
m_texture_farm->bind(12);
|
||||
}
|
||||
// Render using our shader
|
||||
m_program->bind();
|
||||
|
||||
@@ -334,6 +349,18 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
|
||||
m_program->setUniformValue("texRedSand", 9);
|
||||
m_program->setUniformValue("texRedRock", 10);
|
||||
m_program->setUniformValue("texForest", 11);
|
||||
m_program->setUniformValue("texFarm", 12);
|
||||
|
||||
const TileImprovement *imp = GameManager::instance().getImprovementAt(m_x, m_y);
|
||||
bool isFarm = (imp != nullptr && imp->improvementId == "farm");
|
||||
m_program->setUniformValue("isFarm", isFarm ? 1 : 0);
|
||||
|
||||
const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig("farm");
|
||||
float fScale = farmCfg ? farmCfg->textureScale : 0.6f;
|
||||
int mappingMode = (farmCfg && farmCfg->textureMapping == "hex") ? 1 : 0;
|
||||
m_program->setUniformValue("farmTextureScale", fScale);
|
||||
m_program->setUniformValue("farmMappingMode", mappingMode);
|
||||
|
||||
m_program->setUniformValue("time", time);
|
||||
m_program->setUniformValue("riverMask", (int)m_riverMask);
|
||||
m_object.bind();
|
||||
|
||||
@@ -44,8 +44,23 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
|
||||
cfg.cost = root.hasChild("cost") ? root["cost"].asInt(20) : 20;
|
||||
cfg.attackDamage = root.hasChild("attack_damage") ? root["attack_damage"].asInt(0) : 0;
|
||||
cfg.attackRange = root.hasChild("attack_range") ? root["attack_range"].asInt(1) : 1;
|
||||
cfg.buildCharges = root.hasChild("build_charges") ? root["build_charges"].asInt(-1) : -1;
|
||||
cfg.infiniteCharges = root.hasChild("infinite_charges") ? root["infinite_charges"].asBool(cfg.buildCharges < 0) : (cfg.buildCharges < 0);
|
||||
cfg.iconPath = root.hasChild("icon") ? root["icon"].asString() : "";
|
||||
|
||||
if (root.hasChild("domain")) {
|
||||
cfg.movementDomain = stringToMovementDomain(root["domain"].asString());
|
||||
} else if (root.hasChild("movement_domain")) {
|
||||
cfg.movementDomain = stringToMovementDomain(root["movement_domain"].asString());
|
||||
}
|
||||
|
||||
if (root.hasChild("impassable_terrains")) {
|
||||
cfg.impassableTerrains = root["impassable_terrains"].listValues;
|
||||
}
|
||||
if (root.hasChild("passable_terrains")) {
|
||||
cfg.passableTerrains = root["passable_terrains"].listValues;
|
||||
}
|
||||
|
||||
if (root.hasChild("actions")) {
|
||||
cfg.actions = root["actions"].listValues;
|
||||
}
|
||||
@@ -54,6 +69,17 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
|
||||
cfg.actions.prepend("verplaatsen");
|
||||
}
|
||||
|
||||
auto parseActionMap = [&](const QString &key) {
|
||||
if (root.hasChild(key)) {
|
||||
const YamlNode &sNode = root[key];
|
||||
for (auto it = sNode.children.begin(); it != sNode.children.end(); ++it) {
|
||||
cfg.actionSpeeds[it.key()] = it.value().asFloat(10.0f);
|
||||
}
|
||||
}
|
||||
};
|
||||
parseActionMap("action_speeds");
|
||||
parseActionMap("action_durations");
|
||||
|
||||
m_configs[cfg.id] = cfg;
|
||||
return true;
|
||||
}
|
||||
|
||||
+21
-2
@@ -6,6 +6,8 @@
|
||||
#include <QMap>
|
||||
#include <vector>
|
||||
|
||||
#include "terrainmovementmanager.h"
|
||||
|
||||
enum class UnitCategory {
|
||||
Military, // militair
|
||||
Reconnaissance, // verkenning
|
||||
@@ -33,23 +35,40 @@ struct UnitConfig {
|
||||
QString id;
|
||||
QString name;
|
||||
UnitCategory category;
|
||||
MovementDomain movementDomain;
|
||||
int health;
|
||||
float movementSpeed;
|
||||
int cost;
|
||||
int attackDamage;
|
||||
int attackRange; // 1 for melee, >1 for ranged
|
||||
int buildCharges;
|
||||
bool infiniteCharges;
|
||||
QString iconPath;
|
||||
QStringList actions;
|
||||
QMap<QString, float> actionSpeeds; // Duration in seconds per action (e.g. "boerderij_bouwen": 10.0f)
|
||||
QStringList impassableTerrains;
|
||||
QStringList passableTerrains;
|
||||
|
||||
UnitConfig()
|
||||
: id(""), name(""), category(UnitCategory::Builder), health(100),
|
||||
: id(""), name(""), category(UnitCategory::Builder),
|
||||
movementDomain(MovementDomain::Land), health(100),
|
||||
movementSpeed(2.0f), cost(10), attackDamage(0), attackRange(1),
|
||||
iconPath("") {}
|
||||
buildCharges(-1), infiniteCharges(true), iconPath("") {}
|
||||
|
||||
bool isRanged() const { return attackRange > 1; }
|
||||
bool isMilitary() const { return category == UnitCategory::Military; }
|
||||
bool isNaval() const { return movementDomain == MovementDomain::Naval; }
|
||||
bool isLand() const { return movementDomain == MovementDomain::Land; }
|
||||
bool canAttack() const { return actions.contains("aanvallen") || isMilitary(); }
|
||||
bool canBuildCity() const { return actions.contains("steden_bouwen") || actions.contains("build_city"); }
|
||||
bool canBuildFarm() const { return actions.contains("boerderij_bouwen") || actions.contains("build_farm"); }
|
||||
|
||||
float getActionDuration(const QString &actionName, float fallback = 10.0f) const {
|
||||
if (actionSpeeds.contains(actionName)) {
|
||||
return actionSpeeds.value(actionName);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
class UnitConfigManager {
|
||||
|
||||
Reference in New Issue
Block a user