Implement city production projects for units and buildings
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
#include "buildingconfig.h"
|
||||||
|
#include "yamlparser.h"
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
BuildingConfigManager::BuildingConfigManager()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuildingConfigManager::loadConfigFile(const QString &filePath)
|
||||||
|
{
|
||||||
|
YamlNode root = YamlParser::parseFile(filePath);
|
||||||
|
if (root.children.isEmpty()) {
|
||||||
|
qWarning() << "Failed to parse buildings config YAML:" << filePath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse districts
|
||||||
|
if (root.hasChild("districts")) {
|
||||||
|
const YamlNode &districtsNode = root["districts"];
|
||||||
|
for (auto it = districtsNode.children.begin(); it != districtsNode.children.end(); ++it) {
|
||||||
|
DistrictConfig dCfg;
|
||||||
|
dCfg.id = it.value().hasChild("id") ? it.value()["id"].asString() : it.key();
|
||||||
|
dCfg.name = it.value().hasChild("name") ? it.value()["name"].asString() : dCfg.id;
|
||||||
|
dCfg.maxBuildings = it.value().hasChild("max_buildings") ? it.value()["max_buildings"].asInt(10) : 10;
|
||||||
|
dCfg.description = it.value().hasChild("description") ? it.value()["description"].asString() : "";
|
||||||
|
m_districts[dCfg.id] = dCfg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure default center district exists
|
||||||
|
if (!m_districts.contains("center")) {
|
||||||
|
DistrictConfig defaultCenter;
|
||||||
|
defaultCenter.id = "center";
|
||||||
|
defaultCenter.name = "City Center";
|
||||||
|
defaultCenter.maxBuildings = 10;
|
||||||
|
m_districts["center"] = defaultCenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse buildings
|
||||||
|
if (root.hasChild("buildings")) {
|
||||||
|
const YamlNode &buildingsNode = root["buildings"];
|
||||||
|
for (auto it = buildingsNode.children.begin(); it != buildingsNode.children.end(); ++it) {
|
||||||
|
BuildingConfig bCfg;
|
||||||
|
bCfg.id = it.value().hasChild("id") ? it.value()["id"].asString() : it.key();
|
||||||
|
bCfg.name = it.value().hasChild("name") ? it.value()["name"].asString() : bCfg.id;
|
||||||
|
bCfg.district = it.value().hasChild("district") ? it.value()["district"].asString("center") : "center";
|
||||||
|
bCfg.laborCost = it.value().hasChild("labor_cost") ? it.value()["labor_cost"].asInt(1000) : 1000;
|
||||||
|
bCfg.foodModifier = it.value().hasChild("food_modifier") ? it.value()["food_modifier"].asFloat(0.0f) : 0.0f;
|
||||||
|
bCfg.foodBonus = it.value().hasChild("food_bonus") ? it.value()["food_bonus"].asInt(0) : 0;
|
||||||
|
bCfg.laborBonus = it.value().hasChild("labor_bonus") ? it.value()["labor_bonus"].asInt(0) : 0;
|
||||||
|
bCfg.cultureBonus = it.value().hasChild("culture_bonus") ? it.value()["culture_bonus"].asInt(0) : 0;
|
||||||
|
bCfg.scienceBonus = it.value().hasChild("science_bonus") ? it.value()["science_bonus"].asInt(0) : 0;
|
||||||
|
bCfg.faithBonus = it.value().hasChild("faith_bonus") ? it.value()["faith_bonus"].asInt(0) : 0;
|
||||||
|
bCfg.goldBonus = it.value().hasChild("gold_bonus") ? it.value()["gold_bonus"].asInt(0) : 0;
|
||||||
|
bCfg.iconPath = it.value().hasChild("icon") ? it.value()["icon"].asString() : "";
|
||||||
|
bCfg.description = it.value().hasChild("description") ? it.value()["description"].asString() : "";
|
||||||
|
m_buildings[bCfg.id] = bCfg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "BuildingConfigManager loaded" << m_districts.size() << "districts and" << m_buildings.size() << "buildings from" << filePath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuildingConfig* BuildingConfigManager::getBuilding(const QString &id) const
|
||||||
|
{
|
||||||
|
auto it = m_buildings.find(id);
|
||||||
|
if (it != m_buildings.end()) {
|
||||||
|
return &it.value();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DistrictConfig* BuildingConfigManager::getDistrict(const QString &id) const
|
||||||
|
{
|
||||||
|
auto it = m_districts.find(id);
|
||||||
|
if (it != m_districts.end()) {
|
||||||
|
return &it.value();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#ifndef BUILDINGCONFIG_H
|
||||||
|
#define BUILDINGCONFIG_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QMap>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct BuildingConfig {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QString district; // e.g. "center"
|
||||||
|
int laborCost;
|
||||||
|
float foodModifier; // e.g. 0.50f for +50% food production
|
||||||
|
int foodBonus;
|
||||||
|
int laborBonus;
|
||||||
|
int cultureBonus;
|
||||||
|
int scienceBonus;
|
||||||
|
int faithBonus;
|
||||||
|
int goldBonus;
|
||||||
|
QString iconPath;
|
||||||
|
QString description;
|
||||||
|
|
||||||
|
BuildingConfig()
|
||||||
|
: id(""), name(""), district("center"), laborCost(1000),
|
||||||
|
foodModifier(0.0f), foodBonus(0), laborBonus(0), cultureBonus(0),
|
||||||
|
scienceBonus(0), faithBonus(0), goldBonus(0),
|
||||||
|
iconPath(""), description("") {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DistrictConfig {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
int maxBuildings;
|
||||||
|
QString description;
|
||||||
|
|
||||||
|
DistrictConfig()
|
||||||
|
: id("center"), name("City Center"), maxBuildings(10), description("") {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class BuildingConfigManager {
|
||||||
|
public:
|
||||||
|
BuildingConfigManager();
|
||||||
|
|
||||||
|
bool loadConfigFile(const QString &filePath);
|
||||||
|
const BuildingConfig* getBuilding(const QString &id) const;
|
||||||
|
const DistrictConfig* getDistrict(const QString &id) const;
|
||||||
|
const QMap<QString, BuildingConfig>& getAllBuildings() const { return m_buildings; }
|
||||||
|
const QMap<QString, DistrictConfig>& getAllDistricts() const { return m_districts; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
QMap<QString, BuildingConfig> m_buildings;
|
||||||
|
QMap<QString, DistrictConfig> m_districts;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BUILDINGCONFIG_H
|
||||||
@@ -18,6 +18,29 @@ struct CityYieldSummary {
|
|||||||
int gold = 0;
|
int gold = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class CityProjectType {
|
||||||
|
None,
|
||||||
|
Unit,
|
||||||
|
Building
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CityProject {
|
||||||
|
CityProjectType type = CityProjectType::None;
|
||||||
|
QString id = "";
|
||||||
|
QString name = "";
|
||||||
|
int laborCost = 0;
|
||||||
|
int currentLabor = 0;
|
||||||
|
QString district = "center";
|
||||||
|
|
||||||
|
bool isValid() const { return type != CityProjectType::None && !id.isEmpty(); }
|
||||||
|
float getProgress() const {
|
||||||
|
return laborCost > 0 ? std::max(0.0f, std::min(1.0f, (float)currentLabor / (float)laborCost)) : 0.0f;
|
||||||
|
}
|
||||||
|
int getRemainingLabor() const {
|
||||||
|
return std::max(0, laborCost - currentLabor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class City {
|
class City {
|
||||||
public:
|
public:
|
||||||
int id;
|
int id;
|
||||||
@@ -31,12 +54,45 @@ public:
|
|||||||
int inhabitantsPerPop;
|
int inhabitantsPerPop;
|
||||||
int accumulatedLabor = 0; // Local resource (Arbeid)
|
int accumulatedLabor = 0; // Local resource (Arbeid)
|
||||||
|
|
||||||
|
CityProject currentProject;
|
||||||
|
std::vector<QString> completedBuildings; // IDs of completed buildings in the city
|
||||||
|
|
||||||
std::vector<std::pair<int, int>> territory; // Owned hex coordinates
|
std::vector<std::pair<int, int>> territory; // Owned hex coordinates
|
||||||
std::map<std::pair<int, int>, QString> workedTiles; // (x, y) -> resource name
|
std::map<std::pair<int, int>, QString> workedTiles; // (x, y) -> resource name
|
||||||
|
|
||||||
City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos,
|
City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos,
|
||||||
int initInhabitants = 1000, int initInhabitantsPerPop = 100000, int initRadius = 1);
|
int initInhabitants = 1000, int initInhabitantsPerPop = 100000, int initRadius = 1);
|
||||||
|
|
||||||
|
bool hasBuilding(const QString &buildingId) const {
|
||||||
|
for (const auto &b : completedBuildings) {
|
||||||
|
if (b == buildingId) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool addBuilding(const QString &buildingId) {
|
||||||
|
if (hasBuilding(buildingId)) return false;
|
||||||
|
completedBuildings.push_back(buildingId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasActiveProject() const {
|
||||||
|
return currentProject.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
void startProject(CityProjectType type, const QString &projId, const QString &projName, int cost, const QString &district = "center") {
|
||||||
|
currentProject.type = type;
|
||||||
|
currentProject.id = projId;
|
||||||
|
currentProject.name = projName;
|
||||||
|
currentProject.laborCost = std::max(1, cost);
|
||||||
|
currentProject.currentLabor = 0;
|
||||||
|
currentProject.district = district.isEmpty() ? "center" : district;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelProject() {
|
||||||
|
currentProject = CityProject();
|
||||||
|
}
|
||||||
|
|
||||||
bool addTerritory(int tx, int ty) {
|
bool addTerritory(int tx, int ty) {
|
||||||
if (ownsTile(tx, ty)) return false;
|
if (ownsTile(tx, ty)) return false;
|
||||||
territory.emplace_back(tx, ty);
|
territory.emplace_back(tx, ty);
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# City Buildings & Extensions configuration
|
||||||
|
# All gameplay values are configurable via YAML
|
||||||
|
|
||||||
|
districts:
|
||||||
|
center:
|
||||||
|
id: "center"
|
||||||
|
name: "City Center"
|
||||||
|
max_buildings: 10
|
||||||
|
description: "The urban core and administrative center of the city."
|
||||||
|
|
||||||
|
buildings:
|
||||||
|
granary:
|
||||||
|
id: "granary"
|
||||||
|
name: "Granary"
|
||||||
|
district: "center"
|
||||||
|
labor_cost: 1000
|
||||||
|
food_modifier: 0.50
|
||||||
|
food_bonus: 0
|
||||||
|
labor_bonus: 0
|
||||||
|
culture_bonus: 0
|
||||||
|
science_bonus: 0
|
||||||
|
faith_bonus: 0
|
||||||
|
gold_bonus: 0
|
||||||
|
icon: "images/improvements/granary.png"
|
||||||
|
description: "+50% Food production in city"
|
||||||
|
|
||||||
|
monument:
|
||||||
|
id: "monument"
|
||||||
|
name: "Monument"
|
||||||
|
district: "center"
|
||||||
|
labor_cost: 800
|
||||||
|
food_modifier: 0.0
|
||||||
|
food_bonus: 0
|
||||||
|
labor_bonus: 0
|
||||||
|
culture_bonus: 250
|
||||||
|
science_bonus: 0
|
||||||
|
faith_bonus: 0
|
||||||
|
gold_bonus: 0
|
||||||
|
icon: "images/improvements/monument.png"
|
||||||
|
description: "+250 Culture per tick"
|
||||||
@@ -12,6 +12,8 @@ city_view_zoom: 4.8
|
|||||||
base_resource_yield_per_tick: 500
|
base_resource_yield_per_tick: 500
|
||||||
tile_culture_cost: 10000
|
tile_culture_cost: 10000
|
||||||
food_to_inhabitants_ratio: 1.0
|
food_to_inhabitants_ratio: 1.0
|
||||||
|
default_unit_labor_cost: 1000
|
||||||
|
unit_labor_cost_multiplier: 20
|
||||||
|
|
||||||
# Base yield breakdown per resource (default 500 per tick)
|
# Base yield breakdown per resource (default 500 per tick)
|
||||||
base_yields:
|
base_yields:
|
||||||
|
|||||||
@@ -59,3 +59,35 @@ ui:
|
|||||||
produce_unit: "Produce Unit"
|
produce_unit: "Produce Unit"
|
||||||
build_district: "Construct District"
|
build_district: "Construct District"
|
||||||
population_growth: "Growth in {turns}s"
|
population_growth: "Growth in {turns}s"
|
||||||
|
management_title: "%1 - CITY MANAGEMENT"
|
||||||
|
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."
|
||||||
|
current_production: "CURRENT PRODUCTION"
|
||||||
|
no_project: "No active project"
|
||||||
|
select_prompt: "Select a unit or building to construct"
|
||||||
|
ticks_remaining: "%1 ticks remaining (%2 / %3 Labor)"
|
||||||
|
cancel_btn: "Cancel"
|
||||||
|
districts_header: "DISTRICT: CITY CENTER"
|
||||||
|
no_buildings: "No extensions built yet"
|
||||||
|
choose_project: "CHOOSE CITY PROJECT"
|
||||||
|
tab_buildings: "Buildings & Extensions"
|
||||||
|
tab_units: "Units"
|
||||||
|
btn_build: "Build"
|
||||||
|
btn_train: "Train"
|
||||||
|
status_built: "BUILT"
|
||||||
|
status_in_progress: "ACTIVE"
|
||||||
|
labor_cost_label: "%1 Labor (%2 ticks)"
|
||||||
|
unit_trained: "%1: %2 trained!"
|
||||||
|
building_completed: "%1: %2 completed in %3!"
|
||||||
|
project_started: "%1: Started %2"
|
||||||
|
project_cancelled: "%1: Project cancelled"
|
||||||
|
already_built: "%1 already built in this city!"
|
||||||
|
growth_float: "+%1 Inhabitants | +%2 Culture"
|
||||||
|
not_enough_culture: "Not enough Culture! (%1 needed)"
|
||||||
|
worker_freed: "Pop worker freed"
|
||||||
|
worker_assigned: "+1 Pop working"
|
||||||
|
no_free_pop: "No free Pop!"
|
||||||
|
buy_tile: "BUY"
|
||||||
|
occupied: "OCCUPIED"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ domain: land
|
|||||||
health: 100
|
health: 100
|
||||||
movement_speed: 2.0
|
movement_speed: 2.0
|
||||||
cost: 50
|
cost: 50
|
||||||
|
labor_cost: 1000
|
||||||
build_charges: -1 # -1 voor oneindig doorwerken
|
build_charges: -1 # -1 voor oneindig doorwerken
|
||||||
infinite_charges: true
|
infinite_charges: true
|
||||||
icon: images/units/builder.png
|
icon: images/units/builder.png
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ domain: naval
|
|||||||
health: 120
|
health: 120
|
||||||
movement_speed: 3.0
|
movement_speed: 3.0
|
||||||
cost: 60
|
cost: 60
|
||||||
|
labor_cost: 1500
|
||||||
attack_damage: 25
|
attack_damage: 25
|
||||||
attack_range: 1
|
attack_range: 1
|
||||||
icon: images/units/galley.png
|
icon: images/units/galley.png
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ domain: land
|
|||||||
health: 100
|
health: 100
|
||||||
movement_speed: 2.0
|
movement_speed: 2.0
|
||||||
cost: 80
|
cost: 80
|
||||||
|
labor_cost: 2000
|
||||||
icon: images/units/settler.png
|
icon: images/units/settler.png
|
||||||
actions:
|
actions:
|
||||||
- verplaatsen
|
- verplaatsen
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ domain: land
|
|||||||
health: 70
|
health: 70
|
||||||
movement_speed: 2.0
|
movement_speed: 2.0
|
||||||
cost: 35
|
cost: 35
|
||||||
|
labor_cost: 800
|
||||||
attack_damage: 15
|
attack_damage: 15
|
||||||
attack_range: 2
|
attack_range: 2
|
||||||
icon: images/units/slinger.png
|
icon: images/units/slinger.png
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ domain: land
|
|||||||
health: 100
|
health: 100
|
||||||
movement_speed: 2.0
|
movement_speed: 2.0
|
||||||
cost: 40
|
cost: 40
|
||||||
|
labor_cost: 1000
|
||||||
attack_damage: 20
|
attack_damage: 20
|
||||||
attack_range: 1
|
attack_range: 1
|
||||||
icon: images/units/warrior.png
|
icon: images/units/warrior.png
|
||||||
|
|||||||
+132
-7
@@ -42,6 +42,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
|||||||
QString unitsConfigDir = baseDir + "/config/units";
|
QString unitsConfigDir = baseDir + "/config/units";
|
||||||
m_unitConfigManager.loadFromDirectory(unitsConfigDir);
|
m_unitConfigManager.loadFromDirectory(unitsConfigDir);
|
||||||
|
|
||||||
|
// Load building & district YAML configurations
|
||||||
|
QString buildingsConfigPath = baseDir + "/config/buildings.yaml";
|
||||||
|
m_buildingConfigManager.loadConfigFile(buildingsConfigPath);
|
||||||
|
|
||||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||||
|
|
||||||
// Initialize player national resources from configuration
|
// Initialize player national resources from configuration
|
||||||
@@ -272,6 +276,29 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
|
|||||||
else if (assignedRes == "geloof") summary.faith += amount;
|
else if (assignedRes == "geloof") summary.faith += amount;
|
||||||
else if (assignedRes == "goud") summary.gold += amount;
|
else if (assignedRes == "goud") summary.gold += amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Additional yields and multipliers from completed city buildings
|
||||||
|
float totalFoodMultiplier = 1.0f;
|
||||||
|
for (const QString &bId : city.completedBuildings) {
|
||||||
|
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(bId);
|
||||||
|
if (!bCfg) continue;
|
||||||
|
|
||||||
|
summary.food += bCfg->foodBonus;
|
||||||
|
summary.labor += bCfg->laborBonus;
|
||||||
|
summary.science += bCfg->scienceBonus;
|
||||||
|
summary.culture += bCfg->cultureBonus;
|
||||||
|
summary.faith += bCfg->faithBonus;
|
||||||
|
summary.gold += bCfg->goldBonus;
|
||||||
|
|
||||||
|
if (bCfg->foodModifier > 0.0f) {
|
||||||
|
totalFoodMultiplier += bCfg->foodModifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalFoodMultiplier != 1.0f) {
|
||||||
|
summary.food = (int)std::round(summary.food * totalFoodMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,8 +313,15 @@ void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPo
|
|||||||
int foodGrown = (int)std::round(yields.food * cfg.foodToInhabitantsRatio);
|
int foodGrown = (int)std::round(yields.food * cfg.foodToInhabitantsRatio);
|
||||||
city.inhabitants += foodGrown;
|
city.inhabitants += foodGrown;
|
||||||
|
|
||||||
// 2. Labor is accumulated locally in the city:
|
// 2. Labor contributes to active project or accumulates locally:
|
||||||
|
if (city.hasActiveProject()) {
|
||||||
|
city.currentProject.currentLabor += yields.labor;
|
||||||
|
if (city.currentProject.currentLabor >= city.currentProject.laborCost) {
|
||||||
|
completeCityProject(city, mapPoints, numPoints05);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
city.accumulatedLabor += yields.labor;
|
city.accumulatedLabor += yields.labor;
|
||||||
|
}
|
||||||
|
|
||||||
// 3. National resources go to the owner player:
|
// 3. National resources go to the owner player:
|
||||||
Player *owner = m_playerManager.getPlayer(city.playerId);
|
Player *owner = m_playerManager.getPlayer(city.playerId);
|
||||||
@@ -300,8 +334,10 @@ void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPo
|
|||||||
|
|
||||||
// Floating growth text over city
|
// Floating growth text over city
|
||||||
QVector3D ftPos = city.worldPos + QVector3D(0.0f, 0.0f, 1.1f);
|
QVector3D ftPos = city.worldPos + QVector3D(0.0f, 0.0f, 1.1f);
|
||||||
addFloatingText(QString("+%1 Inwoners | +%2 Cultuur").arg(foodGrown).arg(yields.culture),
|
QString growthMsg = Localization::instance().get(
|
||||||
ftPos, QColor(100, 255, 140), 2.5f);
|
"ui.city.growth_float", "+%1 Inhabitants | +%2 Culture")
|
||||||
|
.arg(foodGrown).arg(yields.culture);
|
||||||
|
addFloatingText(growthMsg, ftPos, QColor(100, 255, 140), 2.5f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,8 +378,8 @@ bool GameManager::buyTileForCity(int cityId, int tx, int ty)
|
|||||||
int cost = cfg.tileCultureCost;
|
int cost = cfg.tileCultureCost;
|
||||||
if (player->culture < cost) {
|
if (player->culture < cost) {
|
||||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||||
addFloatingText(QString("Niet genoeg Cultuur! (%1 nodig)").arg(cost),
|
QString notEnough = Localization::instance().get("ui.city.not_enough_culture", "Not enough Culture! (%1 needed)").arg(cost);
|
||||||
QVector3D(w.x(), w.y(), 0.8f), QColor(255, 90, 90), 2.0f);
|
addFloatingText(notEnough, QVector3D(w.x(), w.y(), 0.8f), QColor(255, 90, 90), 2.0f);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,11 +388,100 @@ bool GameManager::buyTileForCity(int cityId, int tx, int ty)
|
|||||||
city->addTerritory(tx, ty);
|
city->addTerritory(tx, ty);
|
||||||
|
|
||||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||||
addFloatingText(QString("Tegel gekocht! (-%1 Cultuur)").arg(cost),
|
QString boughtMsg = Localization::instance().get("ui.unit.tile_purchased", "Tile purchased! (-%1 Culture)").arg(cost);
|
||||||
QVector3D(w.x(), w.y(), 0.8f), QColor(210, 120, 255), 2.5f);
|
addFloatingText(boughtMsg, QVector3D(w.x(), w.y(), 0.8f), QColor(210, 120, 255), 2.5f);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameManager::setCityProject(int cityId, CityProjectType type, const QString &id)
|
||||||
|
{
|
||||||
|
City *city = getCityById(cityId);
|
||||||
|
if (!city) return;
|
||||||
|
|
||||||
|
if (type == CityProjectType::Unit) {
|
||||||
|
const UnitConfig *uCfg = m_unitConfigManager.getConfig(id);
|
||||||
|
if (!uCfg) return;
|
||||||
|
city->startProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost);
|
||||||
|
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
|
||||||
|
.arg(city->name).arg(uCfg->name);
|
||||||
|
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
|
||||||
|
} else if (type == CityProjectType::Building) {
|
||||||
|
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(id);
|
||||||
|
if (!bCfg) return;
|
||||||
|
if (city->hasBuilding(id)) {
|
||||||
|
QString msg = Localization::instance().get("ui.city.already_built", "%1 already built!").arg(bCfg->name);
|
||||||
|
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
city->startProject(CityProjectType::Building, id, bCfg->name, bCfg->laborCost, bCfg->district);
|
||||||
|
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
|
||||||
|
.arg(city->name).arg(bCfg->name);
|
||||||
|
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameManager::cancelCityProject(int cityId)
|
||||||
|
{
|
||||||
|
City *city = getCityById(cityId);
|
||||||
|
if (!city || !city->hasActiveProject()) return;
|
||||||
|
|
||||||
|
QString msg = Localization::instance().get("ui.city.project_cancelled", "%1: Project cancelled").arg(city->name);
|
||||||
|
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 180, 80), 2.0f);
|
||||||
|
city->cancelProject();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameManager::completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||||
|
{
|
||||||
|
if (!city.hasActiveProject()) return;
|
||||||
|
|
||||||
|
if (city.currentProject.type == CityProjectType::Unit) {
|
||||||
|
QString unitId = city.currentProject.id;
|
||||||
|
const UnitConfig *uCfg = m_unitConfigManager.getConfig(unitId);
|
||||||
|
|
||||||
|
// Find spawn location: city center or adjacent empty hex
|
||||||
|
int spawnX = city.tileX;
|
||||||
|
int spawnY = city.tileY;
|
||||||
|
|
||||||
|
if (getUnitAt(spawnX, spawnY) != nullptr) {
|
||||||
|
for (int e = 0; e < 6; ++e) {
|
||||||
|
int nx, ny;
|
||||||
|
getHexNeighbor(city.tileX, city.tileY, e, nx, ny);
|
||||||
|
if (nx >= 0 && nx < MAP_WIDTH && ny >= 0 && ny < MAP_HEIGHT) {
|
||||||
|
if (getUnitAt(nx, ny) == nullptr &&
|
||||||
|
TerrainMovementManager::instance().isHexPassable(nx, ny, uCfg ? uCfg->movementDomain : MovementDomain::Land, mapPoints, numPoints05, uCfg)) {
|
||||||
|
spawnX = nx;
|
||||||
|
spawnY = ny;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Unit *spawned = spawnUnit(unitId, city.playerId, spawnX, spawnY, mapPoints, numPoints05);
|
||||||
|
QString unitName = spawned ? spawned->config.name : (uCfg ? uCfg->name : unitId);
|
||||||
|
QString msg = Localization::instance().get("ui.city.unit_trained", "%1: %2 trained!")
|
||||||
|
.arg(city.name).arg(unitName);
|
||||||
|
addFloatingText(msg, city.worldPos + QVector3D(0.0f, 0.0f, 1.3f), QColor(80, 255, 140), 3.0f);
|
||||||
|
|
||||||
|
} else if (city.currentProject.type == CityProjectType::Building) {
|
||||||
|
QString bId = city.currentProject.id;
|
||||||
|
city.addBuilding(bId);
|
||||||
|
|
||||||
|
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(bId);
|
||||||
|
QString bName = bCfg ? bCfg->name : bId;
|
||||||
|
const DistrictConfig *dCfg = m_buildingConfigManager.getDistrict(bCfg ? bCfg->district : "center");
|
||||||
|
QString dName = dCfg ? dCfg->name : "City Center";
|
||||||
|
|
||||||
|
QString msg = Localization::instance().get("ui.city.building_completed", "%1: %2 completed in %3!")
|
||||||
|
.arg(city.name).arg(bName).arg(dName);
|
||||||
|
addFloatingText(msg, city.worldPos + QVector3D(0.0f, 0.0f, 1.3f), QColor(80, 240, 255), 3.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int excess = city.currentProject.currentLabor - city.currentProject.laborCost;
|
||||||
|
city.accumulatedLabor += std::max(0, excess);
|
||||||
|
city.cancelProject();
|
||||||
|
}
|
||||||
|
|
||||||
void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||||
{
|
{
|
||||||
Unit *selected = getSelectedUnit();
|
Unit *selected = getSelectedUnit();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "unitconfig.h"
|
#include "unitconfig.h"
|
||||||
|
#include "buildingconfig.h"
|
||||||
#include "unit.h"
|
#include "unit.h"
|
||||||
#include "city.h"
|
#include "city.h"
|
||||||
#include "matrix2d.h"
|
#include "matrix2d.h"
|
||||||
@@ -45,6 +46,8 @@ public:
|
|||||||
|
|
||||||
PlayerManager& players() { return m_playerManager; }
|
PlayerManager& players() { return m_playerManager; }
|
||||||
UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
|
UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
|
||||||
|
BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; }
|
||||||
|
const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; }
|
||||||
|
|
||||||
const std::vector<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
|
const std::vector<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
|
||||||
const std::vector<City>& getCities() const { return m_cities; }
|
const std::vector<City>& getCities() const { return m_cities; }
|
||||||
@@ -79,6 +82,10 @@ public:
|
|||||||
bool isTileOwned(int tx, int ty, int *outOwnerCityId = nullptr) const;
|
bool isTileOwned(int tx, int ty, int *outOwnerCityId = nullptr) const;
|
||||||
bool buyTileForCity(int cityId, int tx, int ty);
|
bool buyTileForCity(int cityId, int tx, int ty);
|
||||||
|
|
||||||
|
void setCityProject(int cityId, CityProjectType type, const QString &id);
|
||||||
|
void cancelCityProject(int cityId);
|
||||||
|
void completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||||
|
|
||||||
void commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
void commandMoveSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||||
void commandAttack(Unit *target);
|
void commandAttack(Unit *target);
|
||||||
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||||
@@ -110,6 +117,7 @@ private:
|
|||||||
|
|
||||||
PlayerManager m_playerManager;
|
PlayerManager m_playerManager;
|
||||||
UnitConfigManager m_unitConfigManager;
|
UnitConfigManager m_unitConfigManager;
|
||||||
|
BuildingConfigManager m_buildingConfigManager;
|
||||||
std::vector<std::shared_ptr<Unit>> m_units;
|
std::vector<std::shared_ptr<Unit>> m_units;
|
||||||
std::vector<City> m_cities;
|
std::vector<City> m_cities;
|
||||||
std::vector<FloatingText> m_floatingTexts;
|
std::vector<FloatingText> m_floatingTexts;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 940 B |
Binary file not shown.
|
After Width: | Height: | Size: 650 B |
+10
-3
@@ -1094,6 +1094,10 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
|
|||||||
{
|
{
|
||||||
m_citySlotHits.clear();
|
m_citySlotHits.clear();
|
||||||
m_cityBuyTileHits.clear();
|
m_cityBuyTileHits.clear();
|
||||||
|
m_cityProjectHits.clear();
|
||||||
|
m_cancelProjectBtnRect = QRect();
|
||||||
|
m_tabBuildingsRect = QRect();
|
||||||
|
m_tabUnitsRect = QRect();
|
||||||
City *city = GameManager::instance().getActiveCityView();
|
City *city = GameManager::instance().getActiveCityView();
|
||||||
if (!city) return;
|
if (!city) return;
|
||||||
|
|
||||||
@@ -1230,10 +1234,12 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
|
|||||||
QString costStr = QString("%L1").arg(cultureCost);
|
QString costStr = QString("%L1").arg(cultureCost);
|
||||||
painter.drawText(QRect(buyRect.left() + 22, buyRect.top() + 2, buyRect.width() - 24, 13), Qt::AlignLeft | Qt::AlignVCenter, costStr);
|
painter.drawText(QRect(buyRect.left() + 22, buyRect.top() + 2, buyRect.width() - 24, 13), Qt::AlignLeft | Qt::AlignVCenter, costStr);
|
||||||
|
|
||||||
// "Koop" subtitle
|
// "Buy" subtitle
|
||||||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||||||
painter.setPen(canAfford ? QColor(220, 140, 255) : QColor(140, 120, 155));
|
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");
|
painter.drawText(QRect(buyRect.left() + 22, buyRect.top() + 14, buyRect.width() - 24, 12),
|
||||||
|
Qt::AlignLeft | Qt::AlignVCenter,
|
||||||
|
Localization::instance().get("ui.city.buy_tile", "BUY"));
|
||||||
} else {
|
} else {
|
||||||
// Owned by another city
|
// Owned by another city
|
||||||
QRect lockRect((int)sCenter.x() - 16, (int)sCenter.y() - 8, 32, 16);
|
QRect lockRect((int)sCenter.x() - 16, (int)sCenter.y() - 8, 32, 16);
|
||||||
@@ -1242,7 +1248,8 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
|
|||||||
painter.drawRoundedRect(lockRect, 3, 3);
|
painter.drawRoundedRect(lockRect, 3, 3);
|
||||||
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
painter.setFont(QFont("sans-serif", 6, QFont::Bold));
|
||||||
painter.setPen(QColor(230, 130, 130));
|
painter.setPen(QColor(230, 130, 130));
|
||||||
painter.drawText(lockRect, Qt::AlignCenter, "BEZET");
|
painter.drawText(lockRect, Qt::AlignCenter,
|
||||||
|
Localization::instance().get("ui.city.occupied", "OCCUPIED"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,18 @@ protected:
|
|||||||
int cityId;
|
int cityId;
|
||||||
};
|
};
|
||||||
std::vector<CityBannerHit> m_cityBannerHits;
|
std::vector<CityBannerHit> m_cityBannerHits;
|
||||||
|
|
||||||
|
struct CityProjectHit {
|
||||||
|
QRect rect;
|
||||||
|
CityProjectType type;
|
||||||
|
QString id;
|
||||||
|
};
|
||||||
|
std::vector<CityProjectHit> m_cityProjectHits;
|
||||||
|
|
||||||
|
QRect m_cancelProjectBtnRect;
|
||||||
|
int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units
|
||||||
|
QRect m_tabBuildingsRect;
|
||||||
|
QRect m_tabUnitsRect;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // QSCENERENDERER_H
|
#endif // QSCENERENDERER_H
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
|
|||||||
cfg.health = root.hasChild("health") ? root["health"].asInt(100) : 100;
|
cfg.health = root.hasChild("health") ? root["health"].asInt(100) : 100;
|
||||||
cfg.movementSpeed = root.hasChild("movement_speed") ? root["movement_speed"].asFloat(2.0f) : 2.0f;
|
cfg.movementSpeed = root.hasChild("movement_speed") ? root["movement_speed"].asFloat(2.0f) : 2.0f;
|
||||||
cfg.cost = root.hasChild("cost") ? root["cost"].asInt(20) : 20;
|
cfg.cost = root.hasChild("cost") ? root["cost"].asInt(20) : 20;
|
||||||
|
if (root.hasChild("labor_cost")) {
|
||||||
|
cfg.laborCost = root["labor_cost"].asInt(1000);
|
||||||
|
} else if (root.hasChild("production_cost")) {
|
||||||
|
cfg.laborCost = root["production_cost"].asInt(1000);
|
||||||
|
} else {
|
||||||
|
cfg.laborCost = cfg.cost * 20;
|
||||||
|
}
|
||||||
cfg.attackDamage = root.hasChild("attack_damage") ? root["attack_damage"].asInt(0) : 0;
|
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.attackRange = root.hasChild("attack_range") ? root["attack_range"].asInt(1) : 1;
|
||||||
cfg.buildCharges = root.hasChild("build_charges") ? root["build_charges"].asInt(-1) : -1;
|
cfg.buildCharges = root.hasChild("build_charges") ? root["build_charges"].asInt(-1) : -1;
|
||||||
|
|||||||
+2
-1
@@ -39,6 +39,7 @@ struct UnitConfig {
|
|||||||
int health;
|
int health;
|
||||||
float movementSpeed;
|
float movementSpeed;
|
||||||
int cost;
|
int cost;
|
||||||
|
int laborCost;
|
||||||
int attackDamage;
|
int attackDamage;
|
||||||
int attackRange; // 1 for melee, >1 for ranged
|
int attackRange; // 1 for melee, >1 for ranged
|
||||||
int buildCharges;
|
int buildCharges;
|
||||||
@@ -52,7 +53,7 @@ struct UnitConfig {
|
|||||||
UnitConfig()
|
UnitConfig()
|
||||||
: id(""), name(""), category(UnitCategory::Builder),
|
: id(""), name(""), category(UnitCategory::Builder),
|
||||||
movementDomain(MovementDomain::Land), health(100),
|
movementDomain(MovementDomain::Land), health(100),
|
||||||
movementSpeed(2.0f), cost(10), attackDamage(0), attackRange(1),
|
movementSpeed(2.0f), cost(10), laborCost(1000), attackDamage(0), attackRange(1),
|
||||||
buildCharges(-1), infiniteCharges(true), iconPath("") {}
|
buildCharges(-1), infiniteCharges(true), iconPath("") {}
|
||||||
|
|
||||||
bool isRanged() const { return attackRange > 1; }
|
bool isRanged() const { return attackRange > 1; }
|
||||||
|
|||||||
Reference in New Issue
Block a user