Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1f67be139 | ||
|
|
8c907a5215 | ||
|
|
dfefb2d7fb | ||
|
|
43b8b1556d | ||
|
|
4b237872eb | ||
|
|
cbba54969a | ||
|
|
f8cd221e37 | ||
|
|
ae04619dae | ||
|
|
73acd61c82 | ||
|
|
72234a23a8 |
@@ -0,0 +1,7 @@
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
Makefile*
|
||||
tiletest
|
||||
terrain_test2
|
||||
.qmake.stash
|
||||
@@ -513,22 +513,7 @@ main.o: main.cpp mainwindow.h
|
||||
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o main.o main.cpp
|
||||
|
||||
mainwindow.o: mainwindow.cpp mainwindow.h \
|
||||
ui_mainwindow.h \
|
||||
qscenerenderer.h \
|
||||
terrain.h \
|
||||
tile.h \
|
||||
vertex.h \
|
||||
face.h \
|
||||
tilevertexinfo.h \
|
||||
matrix2d.h \
|
||||
unitrenderer.h \
|
||||
unit.h \
|
||||
unitconfig.h \
|
||||
terrainmovementmanager.h \
|
||||
player.h \
|
||||
city.h \
|
||||
gamemanager.h \
|
||||
terrainyieldmanager.h
|
||||
ui_mainwindow.h
|
||||
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o mainwindow.o mainwindow.cpp
|
||||
|
||||
qscenerenderer.o: qscenerenderer.cpp qscenerenderer.h \
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
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 {
|
||||
public:
|
||||
int id;
|
||||
@@ -31,12 +54,46 @@ public:
|
||||
int inhabitantsPerPop;
|
||||
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::map<std::pair<int, int>, QString> workedTiles; // (x, y) -> resource name
|
||||
std::map<std::pair<int, int>, QString> tilePreferredResources; // (x, y) -> user chosen resource
|
||||
|
||||
City(int cId, int pId, const QString &cName, int tx, int ty, const QVector3D &pos,
|
||||
int initInhabitants = 1000, int initInhabitantsPerPop = 100000, int initRadius = 1);
|
||||
|
||||
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) {
|
||||
if (ownsTile(tx, ty)) return false;
|
||||
territory.emplace_back(tx, ty);
|
||||
@@ -71,8 +128,24 @@ public:
|
||||
return it != workedTiles.end() ? it->second : "";
|
||||
}
|
||||
|
||||
QString getEffectiveResource(int tx, int ty, const QString &defaultBestRes) const {
|
||||
auto itW = workedTiles.find({tx, ty});
|
||||
if (itW != workedTiles.end()) return itW->second;
|
||||
auto itP = tilePreferredResources.find({tx, ty});
|
||||
if (itP != tilePreferredResources.end()) return itP->second;
|
||||
return defaultBestRes;
|
||||
}
|
||||
|
||||
void setTilePreferredResource(int tx, int ty, const QString &resource) {
|
||||
tilePreferredResources[{tx, ty}] = resource;
|
||||
if (isTileWorked(tx, ty)) {
|
||||
workedTiles[{tx, ty}] = resource;
|
||||
}
|
||||
}
|
||||
|
||||
bool assignWorker(int tx, int ty, const QString &resource) {
|
||||
if (!ownsTile(tx, ty)) return false;
|
||||
tilePreferredResources[{tx, ty}] = resource;
|
||||
if (isTileWorked(tx, ty)) {
|
||||
workedTiles[{tx, ty}] = resource;
|
||||
return true;
|
||||
|
||||
@@ -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
|
||||
tile_culture_cost: 10000
|
||||
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_yields:
|
||||
@@ -28,3 +30,15 @@ initial_national_resources:
|
||||
kennis: 0
|
||||
geloof: 0
|
||||
goud: 0
|
||||
|
||||
# Slot long press duration in milliseconds to open resource selection popup
|
||||
slot_long_press_duration_ms: 400
|
||||
|
||||
# Priority order for resources (used for tie-breaking when yields are equal and display order)
|
||||
resource_priority:
|
||||
- "voedsel"
|
||||
- "arbeid"
|
||||
- "goud"
|
||||
- "kennis"
|
||||
- "cultuur"
|
||||
- "geloof"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
movement_tick_duration: 30.0
|
||||
|
||||
# Action queue configuration
|
||||
action_queue:
|
||||
max_size: 5
|
||||
auto_queue_move_when_busy: true
|
||||
allow_action_queueing: true
|
||||
|
||||
# Camera settings
|
||||
camera:
|
||||
min_zoom: 2.0
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Road System Configuration
|
||||
default_road_type: "dirt"
|
||||
build_duration_ticks: 2
|
||||
meander_amplitude: 0.045
|
||||
meander_subdivisions: 5
|
||||
|
||||
types:
|
||||
dirt:
|
||||
id: "dirt"
|
||||
name: "Dirt Road"
|
||||
speed_multiplier: 1.30 # +30% movement speed
|
||||
color: "#c49a62"
|
||||
border_color: "#6b4b23"
|
||||
width: 3.5
|
||||
cost: 5
|
||||
stone:
|
||||
id: "stone"
|
||||
name: "Stone Road"
|
||||
speed_multiplier: 1.60 # +60% movement speed
|
||||
color: "#9aa1a8"
|
||||
border_color: "#464b50"
|
||||
width: 4.0
|
||||
cost: 15
|
||||
rail:
|
||||
id: "rail"
|
||||
name: "Railroad"
|
||||
speed_multiplier: 2.50 # +150% movement speed
|
||||
color: "#3a3c40"
|
||||
border_color: "#181a1c"
|
||||
width: 4.5
|
||||
cost: 30
|
||||
@@ -45,6 +45,32 @@ ui:
|
||||
|
||||
unit:
|
||||
moving: "Moving"
|
||||
move_queued: "Movement queued"
|
||||
queue_cleared: "Queued actions cancelled"
|
||||
queue_full: "Action queue full!"
|
||||
action_queued: "Action queued: %1"
|
||||
action_cancelled: "%1 cancelled"
|
||||
farm_queued: "Build Farm queued on arrival"
|
||||
farm_started: "Constructing Farm..."
|
||||
farm_completed: "Farm completed!"
|
||||
farm_completed_consumed: "Farm completed! (Builder consumed)"
|
||||
farm_completed_charges: "Farm completed! (%1 %2 remaining)"
|
||||
road_queued: "Build Road queued on arrival"
|
||||
road_started: "Constructing %1... (%2 ticks)"
|
||||
road_completed: "%1 completed!"
|
||||
road_already_here: "Tile already has a road!"
|
||||
cannot_build_road_water: "Roads cannot be built on water!"
|
||||
city_queued: "Found City queued on arrival"
|
||||
city_founded: "%1 founded!"
|
||||
already_building: "Already busy building!"
|
||||
busy: "Unit is busy!"
|
||||
cannot_act_busy: "Unit cannot perform action while busy!"
|
||||
tile_occupied: "Tile already has an improvement!"
|
||||
cannot_build_here: "Cannot build here! Allowed on: %1"
|
||||
cannot_build_destination: "Cannot build on destination terrain! Allowed on: %1"
|
||||
no_build_charges: "No build charges remaining!"
|
||||
charge_singular: "charge"
|
||||
charge_plural: "charges"
|
||||
impassable_terrain: "Impassable terrain!"
|
||||
naval_only_water: "Ships can only sail on water!"
|
||||
no_path: "No reachable path!"
|
||||
@@ -53,9 +79,89 @@ ui:
|
||||
defeated: "Defeated!"
|
||||
city_already_here: "A city already exists here!"
|
||||
unsuitable_terrain: "Unsuitable terrain!"
|
||||
civilian_unit: "Civilian unit"
|
||||
military_unit: "Military unit"
|
||||
speed_format: "Speed: %1 hex/tick"
|
||||
cost_format: "Cost: %1 pts"
|
||||
combat_format: "Attack: %1 | Range: %2 hex"
|
||||
building_pct: "Building... %1%"
|
||||
charges_format: "Charges: %1"
|
||||
status_queued: "Queued: %1"
|
||||
btn_move: "Move [M]"
|
||||
btn_attack: "Attack [A]"
|
||||
btn_build_city: "Settle [B]"
|
||||
btn_build_farm: "Farm [F]"
|
||||
btn_farm_queued: "Farm (Queued)"
|
||||
btn_farm_building: "Building %1%"
|
||||
btn_build_road: "Road [R]"
|
||||
btn_road_queued: "Road (Queued)"
|
||||
btn_road_building: "Building %1%"
|
||||
tip_move: "Move (M)"
|
||||
tip_attack: "Attack (A) [Range: %1]"
|
||||
tip_build_city: "Found City (B)"
|
||||
tip_build_farm: "Build Farm (F)"
|
||||
tip_farm_building: "Busy building... %1%"
|
||||
tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)"
|
||||
tip_build_road: "Build Road (R)"
|
||||
tip_road_building: "Busy building road... %1%"
|
||||
tip_road_queued: "Build Road (Queued on arrival - click to cancel)"
|
||||
road_started: "Constructing %1... (%2 ticks)"
|
||||
road_completed: "%1 completed!"
|
||||
road_queued: "Build Road queued on arrival"
|
||||
road_already_here: "Tile already has a road!"
|
||||
cannot_build_road_water: "Roads cannot be built on water!"
|
||||
|
||||
improvement:
|
||||
farm: "Farm"
|
||||
road: "Road"
|
||||
|
||||
city:
|
||||
center: "City Center"
|
||||
produce_unit: "Produce Unit"
|
||||
build_district: "Construct District"
|
||||
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 slot to assign/free Pop | Hold a slot to choose resource | Buy tile with Culture."
|
||||
current_production: "CURRENT PRODUCTION"
|
||||
no_project: "No active project"
|
||||
select_prompt: "Select a unit or building to construct"
|
||||
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"
|
||||
select_resource: "SELECT RESOURCE"
|
||||
best_yield: "BEST"
|
||||
resource_switched: "Producing %1 (+%2)"
|
||||
worker_assigned_res: "+1 Pop: %1 (+%2)"
|
||||
resource_preferred: "Slot set to %1 (+%2)"
|
||||
|
||||
resources:
|
||||
voedsel: "Food"
|
||||
arbeid: "Production"
|
||||
goud: "Gold"
|
||||
kennis: "Science"
|
||||
cultuur: "Culture"
|
||||
geloof: "Faith"
|
||||
|
||||
@@ -7,8 +7,9 @@ mountain_rock_height_km: 2.3 # Height in km >= 2.3 is rocky mountain
|
||||
snow_height_km: 3.1 # Altitude in km >= 3.1 is high peak snow
|
||||
polar_snow_line_enabled: true # Whether latitude reduces snowline altitude
|
||||
polar_snow_line_base_km: 2.5 # Base snowline at temperate latitudes
|
||||
polar_snow_factor_start: 0.88 # Latitude distFromCenter where polar cooling starts (polar caps only)
|
||||
polar_snow_factor_end: 0.98 # Latitude distFromCenter where sea-level snowline is reached
|
||||
polar_snow_factor_start: 0.40 # Latitude distFromCenter where polar cooling starts (matches simple.frag)
|
||||
polar_snow_factor_end: 0.85 # Latitude distFromCenter where sea-level snowline is reached (matches simple.frag)
|
||||
polar_land_ice_threshold: 0.72 # Latitude distFromCenter where sea-level polar ice starts (matches simple.frag)
|
||||
|
||||
# Slope and elevation movement penalties
|
||||
# Elevation difference (deltaH in km) between hex centers
|
||||
|
||||
@@ -5,11 +5,14 @@ domain: land
|
||||
health: 100
|
||||
movement_speed: 2.0
|
||||
cost: 50
|
||||
labor_cost: 1000
|
||||
build_charges: -1 # -1 voor oneindig doorwerken
|
||||
infinite_charges: true
|
||||
icon: images/units/builder.png
|
||||
actions:
|
||||
- verplaatsen
|
||||
- boerderij_bouwen
|
||||
- build_road
|
||||
action_speeds:
|
||||
boerderij_bouwen: 10.0 # Duur in seconden die deze builder unit nodig heeft voor de actie boerderij_bouwen
|
||||
build_road: 2 # Duur in ticks
|
||||
|
||||
@@ -5,6 +5,7 @@ domain: naval
|
||||
health: 120
|
||||
movement_speed: 3.0
|
||||
cost: 60
|
||||
labor_cost: 1500
|
||||
attack_damage: 25
|
||||
attack_range: 1
|
||||
icon: images/units/galley.png
|
||||
|
||||
@@ -5,6 +5,7 @@ domain: land
|
||||
health: 100
|
||||
movement_speed: 2.0
|
||||
cost: 80
|
||||
labor_cost: 2000
|
||||
icon: images/units/settler.png
|
||||
actions:
|
||||
- verplaatsen
|
||||
|
||||
@@ -5,6 +5,7 @@ domain: land
|
||||
health: 70
|
||||
movement_speed: 2.0
|
||||
cost: 35
|
||||
labor_cost: 800
|
||||
attack_damage: 15
|
||||
attack_range: 2
|
||||
icon: images/units/slinger.png
|
||||
|
||||
@@ -5,6 +5,7 @@ domain: land
|
||||
health: 100
|
||||
movement_speed: 2.0
|
||||
cost: 40
|
||||
labor_cost: 1000
|
||||
attack_damage: 20
|
||||
attack_range: 1
|
||||
icon: images/units/warrior.png
|
||||
|
||||
+686
-51
@@ -16,7 +16,9 @@ GameManager& GameManager::instance()
|
||||
GameManager::GameManager()
|
||||
: m_selectedUnitId(-1), m_activeCityViewId(-1), m_actionMode(ActionMode::None),
|
||||
m_movementTickDuration(30.0f), m_tickTimer(0.0f),
|
||||
m_simSpeedMultiplier(1.0f), m_nextUnitId(0), m_nextCityId(0),
|
||||
m_simSpeedMultiplier(1.0f),
|
||||
m_maxActionQueueSize(5), m_autoQueueMoveWhenBusy(true), m_allowActionQueueing(true),
|
||||
m_nextUnitId(0), m_nextCityId(0),
|
||||
m_initialized(false)
|
||||
{
|
||||
}
|
||||
@@ -37,11 +39,25 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
||||
if (gameRoot.hasChild("movement_tick_duration")) {
|
||||
m_movementTickDuration = gameRoot["movement_tick_duration"].asFloat(30.0f);
|
||||
}
|
||||
if (gameRoot.hasChild("action_queue")) {
|
||||
const YamlNode &aqNode = gameRoot["action_queue"];
|
||||
m_maxActionQueueSize = aqNode["max_size"].asInt(5);
|
||||
m_autoQueueMoveWhenBusy = aqNode["auto_queue_move_when_busy"].asBool(true);
|
||||
m_allowActionQueueing = aqNode["allow_action_queueing"].asBool(true);
|
||||
}
|
||||
|
||||
// Load unit YAML configurations
|
||||
QString unitsConfigDir = baseDir + "/config/units";
|
||||
m_unitConfigManager.loadFromDirectory(unitsConfigDir);
|
||||
|
||||
// Load building & district YAML configurations
|
||||
QString buildingsConfigPath = baseDir + "/config/buildings.yaml";
|
||||
m_buildingConfigManager.loadConfigFile(buildingsConfigPath);
|
||||
|
||||
// Load road YAML configurations
|
||||
QString roadsConfigPath = baseDir + "/config/roads.yaml";
|
||||
m_roadConfigManager.loadConfigFile(roadsConfigPath);
|
||||
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
|
||||
// Initialize player national resources from configuration
|
||||
@@ -60,10 +76,8 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
||||
City initialCity(cId, 1, "Roma", 35, 17, QVector3D(w.x(), w.y(), gz),
|
||||
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
|
||||
|
||||
// Assign initial 1 pop to work the city center tile
|
||||
QString tType = TerrainYieldManager::instance().getTerrainType(35, 17, mapPoints, numPoints05);
|
||||
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
|
||||
QString defRes = yCfg ? yCfg->primaryResource : "voedsel";
|
||||
// Assign initial 1 pop to work the city center tile with best yield resource
|
||||
QString defRes = getBestTileResource(35, 17, mapPoints, numPoints05);
|
||||
initialCity.assignWorker(35, 17, defRes);
|
||||
|
||||
m_cities.push_back(initialCity);
|
||||
@@ -98,6 +112,7 @@ Unit* GameManager::spawnUnit(const QString &configId, int playerId, int tileX, i
|
||||
|
||||
int id = getNextUnitId();
|
||||
auto unit = std::make_shared<Unit>(id, playerId, *cfg, tileX, tileY, gz);
|
||||
unit->maxQueueSize = m_maxActionQueueSize;
|
||||
m_units.push_back(unit);
|
||||
return unit.get();
|
||||
}
|
||||
@@ -118,6 +133,9 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
|
||||
if (u->isBuilding() && u->buildProgress >= 1.0f) {
|
||||
completeUnitBuilding(*u, mapPoints, numPoints05);
|
||||
}
|
||||
if (!u->isDead() && u->state == UnitState::Idle && u->hasQueuedAction()) {
|
||||
processUnitActionQueue(*u, mapPoints, numPoints05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +249,67 @@ void GameManager::exitCityView()
|
||||
m_activeCityViewId = -1;
|
||||
}
|
||||
|
||||
QMap<QString, int> GameManager::getTileYields(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
|
||||
{
|
||||
QMap<QString, int> yields;
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
QStringList resources = cCfg.resourcePriority;
|
||||
if (resources.isEmpty()) {
|
||||
resources = {"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"};
|
||||
}
|
||||
|
||||
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
|
||||
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
|
||||
const TileImprovement *imp = getImprovementAt(tx, ty);
|
||||
const ImprovementConfig *iCfg = imp ? TerrainYieldManager::instance().getImprovementConfig(imp->improvementId) : nullptr;
|
||||
|
||||
for (const QString &res : resources) {
|
||||
int amount = yCfg ? yCfg->getYield(res) : 0;
|
||||
if (iCfg && iCfg->yieldBonus.contains(res)) {
|
||||
amount += iCfg->yieldBonus.value(res, 0);
|
||||
}
|
||||
yields[res] = amount;
|
||||
}
|
||||
return yields;
|
||||
}
|
||||
|
||||
int GameManager::getTileYield(int tx, int ty, const QString &res, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
|
||||
{
|
||||
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
|
||||
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
|
||||
int amount = yCfg ? yCfg->getYield(res) : 0;
|
||||
const TileImprovement *imp = getImprovementAt(tx, ty);
|
||||
if (imp) {
|
||||
const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId);
|
||||
if (iCfg && iCfg->yieldBonus.contains(res)) {
|
||||
amount += iCfg->yieldBonus.value(res, 0);
|
||||
}
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
QString GameManager::getBestTileResource(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
|
||||
{
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
QStringList priority = cCfg.resourcePriority;
|
||||
if (priority.isEmpty()) {
|
||||
priority = {"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"};
|
||||
}
|
||||
|
||||
QMap<QString, int> yields = getTileYields(tx, ty, mapPoints, numPoints05);
|
||||
QString bestRes = priority.first();
|
||||
int maxYield = -1;
|
||||
|
||||
for (const QString &res : priority) {
|
||||
int y = yields.value(res, 0);
|
||||
if (y > maxYield) {
|
||||
maxYield = y;
|
||||
bestRes = res;
|
||||
}
|
||||
}
|
||||
return bestRes;
|
||||
}
|
||||
|
||||
CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const
|
||||
{
|
||||
CityYieldSummary summary;
|
||||
@@ -250,20 +329,7 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
|
||||
int ty = pair.first.second;
|
||||
QString assignedRes = pair.second;
|
||||
|
||||
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
|
||||
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
|
||||
if (!yCfg) continue;
|
||||
|
||||
int amount = yCfg->getYield(assignedRes);
|
||||
|
||||
// Add bonus from tile improvements (e.g. farm bonus)
|
||||
const TileImprovement *imp = getImprovementAt(tx, ty);
|
||||
if (imp) {
|
||||
const ImprovementConfig *iCfg = TerrainYieldManager::instance().getImprovementConfig(imp->improvementId);
|
||||
if (iCfg && iCfg->yieldBonus.contains(assignedRes)) {
|
||||
amount += iCfg->yieldBonus.value(assignedRes, 0);
|
||||
}
|
||||
}
|
||||
int amount = getTileYield(tx, ty, assignedRes, mapPoints, numPoints05);
|
||||
|
||||
if (assignedRes == "voedsel") summary.food += amount;
|
||||
else if (assignedRes == "arbeid") summary.labor += amount;
|
||||
@@ -272,6 +338,29 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
|
||||
else if (assignedRes == "geloof") summary.faith += 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;
|
||||
}
|
||||
|
||||
@@ -286,8 +375,15 @@ void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPo
|
||||
int foodGrown = (int)std::round(yields.food * cfg.foodToInhabitantsRatio);
|
||||
city.inhabitants += foodGrown;
|
||||
|
||||
// 2. Labor is accumulated locally in the city:
|
||||
city.accumulatedLabor += yields.labor;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 3. National resources go to the owner player:
|
||||
Player *owner = m_playerManager.getPlayer(city.playerId);
|
||||
@@ -300,8 +396,10 @@ void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPo
|
||||
|
||||
// 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);
|
||||
QString growthMsg = Localization::instance().get(
|
||||
"ui.city.growth_float", "+%1 Inhabitants | +%2 Culture")
|
||||
.arg(foodGrown).arg(yields.culture);
|
||||
addFloatingText(growthMsg, ftPos, QColor(100, 255, 140), 2.5f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,8 +440,8 @@ bool GameManager::buyTileForCity(int cityId, int tx, int ty)
|
||||
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);
|
||||
QString notEnough = Localization::instance().get("ui.city.not_enough_culture", "Not enough Culture! (%1 needed)").arg(cost);
|
||||
addFloatingText(notEnough, QVector3D(w.x(), w.y(), 0.8f), QColor(255, 90, 90), 2.0f);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -352,11 +450,100 @@ bool GameManager::buyTileForCity(int cityId, int tx, int ty)
|
||||
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);
|
||||
QString boughtMsg = Localization::instance().get("ui.unit.tile_purchased", "Tile purchased! (-%1 Culture)").arg(cost);
|
||||
addFloatingText(boughtMsg, QVector3D(w.x(), w.y(), 0.8f), QColor(210, 120, 255), 2.5f);
|
||||
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)
|
||||
{
|
||||
Unit *selected = getSelectedUnit();
|
||||
@@ -379,12 +566,30 @@ void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<Terrain
|
||||
return;
|
||||
}
|
||||
|
||||
// If unit is busy building, it CANNOT walk right now. Queue the move action!
|
||||
if (selected->isBuilding()) {
|
||||
if (m_allowActionQueueing && m_autoQueueMoveWhenBusy) {
|
||||
if (selected->queueAction(UnitAction(UnitActionType::Move, targetX, targetY))) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.move_queued", "Movement queued"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
|
||||
} else {
|
||||
addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
|
||||
}
|
||||
} else {
|
||||
addFloatingText(Localization::instance().get("ui.unit.busy", "Unit is busy!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
|
||||
selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05,
|
||||
selected->config.movementDomain, &selected->config
|
||||
);
|
||||
|
||||
if (path.size() > 1) {
|
||||
selected->clearActionQueue();
|
||||
selected->setPath(path);
|
||||
addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
|
||||
@@ -399,8 +604,15 @@ void GameManager::commandAttack(Unit *target)
|
||||
Unit *selected = getSelectedUnit();
|
||||
if (!selected || !target) return;
|
||||
|
||||
if (selected->isBuilding()) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.cannot_act_busy", "Unit cannot perform action while busy!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selected->canAttack(*target)) {
|
||||
addFloatingText("Buiten bereik!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
addFloatingText(Localization::instance().get("ui.unit.out_of_range", "Out of range!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,7 +623,8 @@ void GameManager::commandAttack(Unit *target)
|
||||
addFloatingText(dmgText, target->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 50, 50));
|
||||
|
||||
if (target->isDead()) {
|
||||
addFloatingText("Verslagen!", target->worldPos + QVector3D(0, 0, 1.2f), QColor(255, 200, 50), 2.2f);
|
||||
addFloatingText(Localization::instance().get("ui.unit.defeated", "Defeated!"),
|
||||
target->worldPos + QVector3D(0, 0, 1.2f), QColor(255, 200, 50), 2.2f);
|
||||
}
|
||||
|
||||
m_actionMode = ActionMode::None;
|
||||
@@ -422,32 +635,60 @@ void GameManager::commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, in
|
||||
Unit *selected = getSelectedUnit();
|
||||
if (!selected || !selected->config.canBuildCity()) return;
|
||||
|
||||
if (selected->isBuilding()) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.cannot_act_busy", "Unit cannot perform action while busy!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
// If unit is moving, queue city founding upon arrival
|
||||
if (selected->state == UnitState::Moving) {
|
||||
if (selected->hasQueuedActionType(UnitActionType::BuildCity)) {
|
||||
selected->removeQueuedActionsOfType(UnitActionType::BuildCity);
|
||||
addFloatingText(Localization::instance().get("ui.unit.action_cancelled", "%1 cancelled").arg("Found City"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected->queueAction(UnitAction(UnitActionType::BuildCity, selected->targetTileX, selected->targetTileY))) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.city_queued", "Found City queued on arrival"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(80, 220, 120), 2.2f);
|
||||
} else {
|
||||
addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int tx = selected->tileX;
|
||||
int ty = selected->tileY;
|
||||
|
||||
// Check if tile already has a city
|
||||
for (const auto &c : m_cities) {
|
||||
if (c.tileX == selected->tileX && c.tileY == selected->tileY) {
|
||||
addFloatingText("Al een stad hier!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
if (c.tileX == tx && c.tileY == ty) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.city_already_here", "A city already exists here!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
int cId = getNextCityId();
|
||||
QString cityName = QString("Stad %1").arg(cId);
|
||||
QVector2D w = Pathfinder::hexToWorld(selected->tileX, selected->tileY);
|
||||
QString cityName = QString("City %1").arg(cId);
|
||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
|
||||
|
||||
City newCity(cId, selected->playerId, cityName, selected->tileX, selected->tileY,
|
||||
City newCity(cId, selected->playerId, cityName, tx, ty,
|
||||
QVector3D(w.x(), w.y(), gz),
|
||||
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
|
||||
|
||||
// Assign initial worker to center tile
|
||||
QString tType = TerrainYieldManager::instance().getTerrainType(selected->tileX, selected->tileY, mapPoints, numPoints05);
|
||||
const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType);
|
||||
QString defRes = yCfg ? yCfg->primaryResource : "voedsel";
|
||||
newCity.assignWorker(selected->tileX, selected->tileY, defRes);
|
||||
// Assign initial worker to center tile with best yield resource
|
||||
QString defRes = getBestTileResource(tx, ty, mapPoints, numPoints05);
|
||||
newCity.assignWorker(tx, ty, defRes);
|
||||
|
||||
m_cities.push_back(newCity);
|
||||
addFloatingText(QString("%1 gesticht!").arg(cityName), QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f);
|
||||
addFloatingText(Localization::instance().get("ui.unit.city_founded", "%1 founded!").arg(cityName),
|
||||
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f);
|
||||
|
||||
// Consuming settler
|
||||
selected->takeDamage(99999.0f);
|
||||
@@ -474,13 +715,144 @@ bool GameManager::hasImprovementAt(int tx, int ty) const
|
||||
return m_tileImprovements.find({tx, ty}) != m_tileImprovements.end();
|
||||
}
|
||||
|
||||
const TileRoad* GameManager::getRoadAt(int tx, int ty) const
|
||||
{
|
||||
auto it = m_tileRoads.find({tx, ty});
|
||||
if (it != m_tileRoads.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool GameManager::hasRoadAt(int tx, int ty) const
|
||||
{
|
||||
return m_tileRoads.find({tx, ty}) != m_tileRoads.end();
|
||||
}
|
||||
|
||||
void GameManager::updateRoadConnections(int tx, int ty)
|
||||
{
|
||||
auto it = m_tileRoads.find({tx, ty});
|
||||
if (it == m_tileRoads.end()) return;
|
||||
|
||||
unsigned char mask = 0;
|
||||
for (int e = 0; e < 6; ++e) {
|
||||
int nx, ny;
|
||||
getHexNeighbor(tx, ty, e, nx, ny);
|
||||
if (nx >= 0 && nx < MAP_WIDTH && ny >= 0 && ny < MAP_HEIGHT) {
|
||||
auto nIt = m_tileRoads.find({nx, ny});
|
||||
if (nIt != m_tileRoads.end()) {
|
||||
mask |= (1 << e);
|
||||
// Also update the neighbor's opposite edge (e + 3) % 6
|
||||
nIt->second.connectionMask |= (1 << ((e + 3) % 6));
|
||||
}
|
||||
}
|
||||
}
|
||||
it->second.connectionMask = mask;
|
||||
}
|
||||
|
||||
void GameManager::addRoad(int tx, int ty, int playerId, const QString &roadTypeId, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
|
||||
|
||||
TileRoad road;
|
||||
road.roadTypeId = roadTypeId.isEmpty() ? m_roadConfigManager.defaultRoadType() : roadTypeId;
|
||||
road.tileX = tx;
|
||||
road.tileY = ty;
|
||||
road.playerId = playerId;
|
||||
road.worldPos = QVector3D(w.x(), w.y(), gz);
|
||||
road.connectionMask = 0;
|
||||
|
||||
m_tileRoads[{tx, ty}] = road;
|
||||
updateRoadConnections(tx, ty);
|
||||
}
|
||||
|
||||
void GameManager::removeRoad(int tx, int ty)
|
||||
{
|
||||
auto it = m_tileRoads.find({tx, ty});
|
||||
if (it == m_tileRoads.end()) return;
|
||||
|
||||
for (int e = 0; e < 6; ++e) {
|
||||
int nx, ny;
|
||||
getHexNeighbor(tx, ty, e, nx, ny);
|
||||
auto nIt = m_tileRoads.find({nx, ny});
|
||||
if (nIt != m_tileRoads.end()) {
|
||||
nIt->second.connectionMask &= ~(1 << ((e + 3) % 6));
|
||||
}
|
||||
}
|
||||
m_tileRoads.erase(it);
|
||||
}
|
||||
|
||||
float GameManager::getRoadSpeedMultiplierBetween(int fromX, int fromY, int toX, int toY) const
|
||||
{
|
||||
auto itFrom = m_tileRoads.find({fromX, fromY});
|
||||
auto itTo = m_tileRoads.find({toX, toY});
|
||||
|
||||
if (itFrom != m_tileRoads.end() && itTo != m_tileRoads.end()) {
|
||||
const RoadTypeConfig *cfgFrom = m_roadConfigManager.getRoadType(itFrom->second.roadTypeId);
|
||||
const RoadTypeConfig *cfgTo = m_roadConfigManager.getRoadType(itTo->second.roadTypeId);
|
||||
float multFrom = cfgFrom ? cfgFrom->speedMultiplier : 1.30f;
|
||||
float multTo = cfgTo ? cfgTo->speedMultiplier : 1.30f;
|
||||
return std::min(multFrom, multTo);
|
||||
}
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
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));
|
||||
addFloatingText(Localization::instance().get("ui.unit.already_building", "Already busy building!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 200, 70));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selected->config.infiniteCharges && selected->buildCharges <= 0) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.no_build_charges", "No build charges remaining!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
// If unit is currently moving: toggle or queue build action on arrival!
|
||||
if (selected->state == UnitState::Moving) {
|
||||
if (selected->hasQueuedActionType(UnitActionType::BuildImprovement)) {
|
||||
selected->removeQueuedActionsOfType(UnitActionType::BuildImprovement);
|
||||
addFloatingText(Localization::instance().get("ui.unit.action_cancelled", "%1 cancelled").arg("Build Farm"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
int destX = selected->targetTileX;
|
||||
int destY = selected->targetTileY;
|
||||
|
||||
// Check if destination tile already has an improvement
|
||||
if (hasImprovementAt(destX, destY)) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.tile_occupied", "Tile already has an improvement!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check destination terrain type against allowed terrains
|
||||
QString tType = TerrainYieldManager::instance().getTerrainType(destX, destY, mapPoints, numPoints05);
|
||||
const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig("farm");
|
||||
QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"};
|
||||
|
||||
if (!validTerrains.contains(tType)) {
|
||||
QString validStr = validTerrains.join(", ");
|
||||
addFloatingText(Localization::instance().get("ui.unit.cannot_build_destination", "Cannot build on destination terrain! Allowed on: %1").arg(validStr),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected->queueAction(UnitAction(UnitActionType::BuildImprovement, destX, destY, "farm"))) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.farm_queued", "Build Farm queued on arrival"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(80, 220, 120), 2.2f);
|
||||
} else {
|
||||
addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -489,7 +861,8 @@ void GameManager::commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, in
|
||||
|
||||
// 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));
|
||||
addFloatingText(Localization::instance().get("ui.unit.tile_occupied", "Tile already has an improvement!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -499,14 +872,91 @@ void GameManager::commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, in
|
||||
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));
|
||||
QString validStr = validTerrains.join(", ");
|
||||
addFloatingText(Localization::instance().get("ui.unit.cannot_build_here", "Cannot build here! Allowed on: %1").arg(validStr),
|
||||
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);
|
||||
addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm..."),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
|
||||
}
|
||||
|
||||
void GameManager::commandBuildRoadSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
Unit *selected = getSelectedUnit();
|
||||
if (!selected || !selected->config.canBuildRoad()) return;
|
||||
|
||||
if (selected->isBuilding()) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.already_building", "Already busy building!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 200, 70));
|
||||
return;
|
||||
}
|
||||
|
||||
QString defaultType = m_roadConfigManager.defaultRoadType();
|
||||
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType);
|
||||
QString roadName = rCfg ? rCfg->name : "Road";
|
||||
|
||||
// If unit is currently moving: toggle or queue build road action on arrival!
|
||||
if (selected->state == UnitState::Moving) {
|
||||
if (selected->hasQueuedActionType(UnitActionType::BuildRoad)) {
|
||||
selected->removeQueuedActionsOfType(UnitActionType::BuildRoad);
|
||||
addFloatingText(Localization::instance().get("ui.unit.action_cancelled", "%1 cancelled").arg("Build Road"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
int destX = selected->targetTileX;
|
||||
int destY = selected->targetTileY;
|
||||
|
||||
if (hasRoadAt(destX, destY)) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.road_already_here", "Tile already has a road!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TerrainMovementManager::instance().isHexPassable(
|
||||
destX, destY, MovementDomain::Land, mapPoints, numPoints05, &selected->config)) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.cannot_build_road_water", "Roads cannot be built on water!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected->queueAction(UnitAction(UnitActionType::BuildRoad, destX, destY, defaultType))) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.road_queued", "Build Road queued on arrival"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(80, 220, 120), 2.2f);
|
||||
} else {
|
||||
addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int tx = selected->tileX;
|
||||
int ty = selected->tileY;
|
||||
|
||||
if (hasRoadAt(tx, ty)) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.road_already_here", "Tile already has a road!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TerrainMovementManager::instance().isHexPassable(
|
||||
tx, ty, MovementDomain::Land, mapPoints, numPoints05, &selected->config)) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.cannot_build_road_water", "Roads cannot be built on water!"),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
// Duration is 2 ticks (configured via YAML)
|
||||
int ticks = m_roadConfigManager.buildDurationTicks();
|
||||
float duration = (float)ticks * m_movementTickDuration;
|
||||
selected->startBuilding(QString("road:%1").arg(defaultType), duration);
|
||||
addFloatingText(Localization::instance().get("ui.unit.road_started", "Constructing %1... (%2 ticks)").arg(roadName).arg(ticks),
|
||||
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
|
||||
}
|
||||
|
||||
void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
@@ -515,15 +965,31 @@ void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPo
|
||||
int ty = unit.tileY;
|
||||
QString impId = unit.buildingImprovementId.isEmpty() ? "farm" : unit.buildingImprovementId;
|
||||
|
||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
|
||||
|
||||
// Check if completing a road
|
||||
if (impId.startsWith("road")) {
|
||||
QString roadType = "dirt";
|
||||
if (impId.contains(":")) {
|
||||
roadType = impId.section(":", 1);
|
||||
}
|
||||
addRoad(tx, ty, unit.playerId, roadType, mapPoints, numPoints05);
|
||||
unit.cancelBuilding();
|
||||
|
||||
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(roadType);
|
||||
QString roadName = rCfg ? rCfg->name : "Road";
|
||||
addFloatingText(Localization::instance().get("ui.unit.road_completed", "%1 completed!").arg(roadName),
|
||||
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(240, 210, 100), 2.8f);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -537,7 +1003,8 @@ void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPo
|
||||
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);
|
||||
addFloatingText(Localization::instance().get("ui.unit.farm_completed_consumed", "Farm completed! (Builder consumed)"),
|
||||
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;
|
||||
@@ -545,14 +1012,182 @@ void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPo
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
QString msg = QString("Boerderij voltooid! (%1 %2 over)")
|
||||
QString chargeWord = unit.buildCharges == 1
|
||||
? Localization::instance().get("ui.unit.charge_singular", "charge")
|
||||
: Localization::instance().get("ui.unit.charge_plural", "charges");
|
||||
QString msg = Localization::instance().get("ui.unit.farm_completed_charges", "Farm completed! (%1 %2 remaining)")
|
||||
.arg(unit.buildCharges)
|
||||
.arg(unit.buildCharges == 1 ? "lading" : "ladingen");
|
||||
.arg(chargeWord);
|
||||
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);
|
||||
addFloatingText(Localization::instance().get("ui.unit.farm_completed", "Farm completed!"),
|
||||
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f);
|
||||
}
|
||||
|
||||
void GameManager::processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
if (unit.isDead() || unit.state != UnitState::Idle || !unit.hasQueuedAction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UnitAction action = unit.popNextQueuedAction();
|
||||
|
||||
switch (action.type) {
|
||||
case UnitActionType::Move: {
|
||||
int tx = action.targetX;
|
||||
int ty = action.targetY;
|
||||
if (tx == unit.tileX && ty == unit.tileY) {
|
||||
return;
|
||||
}
|
||||
if (!TerrainMovementManager::instance().isHexPassable(
|
||||
tx, ty, unit.config.movementDomain, mapPoints, numPoints05, &unit.config)) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.impassable_terrain", "Impassable terrain!"),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, int>> path = Pathfinder::findPath(
|
||||
unit.tileX, unit.tileY, tx, ty, mapPoints, numPoints05,
|
||||
unit.config.movementDomain, &unit.config
|
||||
);
|
||||
|
||||
if (path.size() > 1) {
|
||||
unit.setPath(path);
|
||||
addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
|
||||
} else {
|
||||
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UnitActionType::BuildImprovement: {
|
||||
QString impId = action.extraData.isEmpty() ? "farm" : action.extraData;
|
||||
int tx = unit.tileX;
|
||||
int ty = unit.tileY;
|
||||
|
||||
if (!unit.config.canBuildFarm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!unit.config.infiniteCharges && unit.buildCharges <= 0) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.no_build_charges", "No build charges remaining!"),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasImprovementAt(tx, ty)) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.tile_occupied", "Tile already has an improvement!"),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
|
||||
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
|
||||
const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig(impId);
|
||||
QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"};
|
||||
|
||||
if (!validTerrains.contains(tType)) {
|
||||
QString validStr = validTerrains.join(", ");
|
||||
addFloatingText(Localization::instance().get("ui.unit.cannot_build_here", "Cannot build here! Allowed on: %1").arg(validStr),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
float duration = unit.config.getActionDuration("boerderij_bouwen", 10.0f);
|
||||
unit.startBuilding(impId, duration);
|
||||
addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm..."),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
|
||||
break;
|
||||
}
|
||||
case UnitActionType::BuildCity: {
|
||||
if (!unit.config.canBuildCity()) {
|
||||
return;
|
||||
}
|
||||
int tx = unit.tileX;
|
||||
int ty = unit.tileY;
|
||||
|
||||
// Check if tile already has a city
|
||||
for (const auto &c : m_cities) {
|
||||
if (c.tileX == tx && c.tileY == ty) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.city_already_here", "A city already exists here!"),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
int cId = getNextCityId();
|
||||
QString cityName = QString("City %1").arg(cId);
|
||||
QVector2D w = Pathfinder::hexToWorld(tx, ty);
|
||||
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
|
||||
|
||||
City newCity(cId, unit.playerId, cityName, tx, ty,
|
||||
QVector3D(w.x(), w.y(), gz),
|
||||
cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius);
|
||||
|
||||
QString defRes = getBestTileResource(tx, ty, mapPoints, numPoints05);
|
||||
newCity.assignWorker(tx, ty, defRes);
|
||||
|
||||
m_cities.push_back(newCity);
|
||||
addFloatingText(Localization::instance().get("ui.unit.city_founded", "%1 founded!").arg(cityName),
|
||||
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f);
|
||||
|
||||
unit.takeDamage(99999.0f);
|
||||
if (unit.id == m_selectedUnitId) {
|
||||
m_selectedUnitId = -1;
|
||||
m_actionMode = ActionMode::None;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UnitActionType::BuildRoad: {
|
||||
int tx = unit.tileX;
|
||||
int ty = unit.tileY;
|
||||
if (hasRoadAt(tx, ty)) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.road_already_here", "Tile already has a road!"),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
|
||||
return;
|
||||
}
|
||||
if (!TerrainMovementManager::instance().isHexPassable(
|
||||
tx, ty, MovementDomain::Land, mapPoints, numPoints05, &unit.config)) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.cannot_build_road_water", "Roads cannot be built on water!"),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80));
|
||||
return;
|
||||
}
|
||||
|
||||
QString roadType = action.extraData.isEmpty() ? m_roadConfigManager.defaultRoadType() : action.extraData;
|
||||
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(roadType);
|
||||
QString roadName = rCfg ? rCfg->name : "Road";
|
||||
int ticks = m_roadConfigManager.buildDurationTicks();
|
||||
float duration = (float)ticks * m_movementTickDuration;
|
||||
unit.startBuilding(QString("road:%1").arg(roadType), duration);
|
||||
addFloatingText(Localization::instance().get("ui.unit.road_started", "Constructing %1... (%2 ticks)").arg(roadName).arg(ticks),
|
||||
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
|
||||
break;
|
||||
}
|
||||
case UnitActionType::Attack: {
|
||||
Unit *target = nullptr;
|
||||
for (auto &other : m_units) {
|
||||
if (other->id == action.targetUnitId && !other->isDead()) {
|
||||
target = other.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target && unit.canAttack(*target)) {
|
||||
unit.performAttack(*target);
|
||||
addFloatingText(QString("-%1 HP").arg(unit.config.attackDamage),
|
||||
target->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 50, 50));
|
||||
if (target->isDead()) {
|
||||
addFloatingText(Localization::instance().get("ui.unit.defeated", "Defeated!"),
|
||||
target->worldPos + QVector3D(0, 0, 1.2f), QColor(255, 200, 50), 2.2f);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+55
-1
@@ -6,18 +6,39 @@
|
||||
#include <QString>
|
||||
#include "player.h"
|
||||
#include "unitconfig.h"
|
||||
#include "buildingconfig.h"
|
||||
#include "roadconfig.h"
|
||||
#include "unit.h"
|
||||
#include "city.h"
|
||||
#include "matrix2d.h"
|
||||
#include "terrain.h"
|
||||
#include "terrainyieldmanager.h"
|
||||
#include "roadconfig.h"
|
||||
|
||||
enum class ActionMode {
|
||||
None,
|
||||
Move,
|
||||
Attack,
|
||||
BuildCity,
|
||||
BuildFarm
|
||||
BuildFarm,
|
||||
BuildRoad
|
||||
};
|
||||
|
||||
struct TileRoad {
|
||||
QString roadTypeId = "dirt"; // "dirt", "stone", "rail"
|
||||
int tileX = 0;
|
||||
int tileY = 0;
|
||||
int playerId = 1;
|
||||
unsigned char connectionMask = 0; // 6 bits: bit e (0..5) is 1 if connected to neighbor across edge e
|
||||
QVector3D worldPos;
|
||||
|
||||
bool hasConnection(int edge) const {
|
||||
return (connectionMask & (1 << (edge % 6))) != 0;
|
||||
}
|
||||
void setConnection(int edge, bool connected) {
|
||||
if (connected) connectionMask |= (1 << (edge % 6));
|
||||
else connectionMask &= ~(1 << (edge % 6));
|
||||
}
|
||||
};
|
||||
|
||||
struct TileImprovement {
|
||||
@@ -45,6 +66,10 @@ public:
|
||||
|
||||
PlayerManager& players() { return m_playerManager; }
|
||||
UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
|
||||
BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; }
|
||||
const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; }
|
||||
RoadConfigManager& roadConfigs() { return m_roadConfigManager; }
|
||||
const RoadConfigManager& roadConfigs() const { return m_roadConfigManager; }
|
||||
|
||||
const std::vector<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
|
||||
const std::vector<City>& getCities() const { return m_cities; }
|
||||
@@ -76,19 +101,37 @@ public:
|
||||
CityYieldSummary calculateCityYields(const City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
|
||||
void processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
|
||||
QMap<QString, int> getTileYields(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
|
||||
int getTileYield(int tx, int ty, const QString &res, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
|
||||
QString getBestTileResource(int tx, int ty, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) const;
|
||||
|
||||
bool isTileOwned(int tx, int ty, int *outOwnerCityId = nullptr) const;
|
||||
bool buyTileForCity(int cityId, int tx, int ty);
|
||||
|
||||
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 commandAttack(Unit *target);
|
||||
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
void commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
void commandBuildRoadSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
void completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
void processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
|
||||
const std::map<std::pair<int, int>, TileImprovement>& getTileImprovements() const { return m_tileImprovements; }
|
||||
const TileImprovement* getImprovementAt(int tx, int ty) const;
|
||||
bool hasImprovementAt(int tx, int ty) const;
|
||||
|
||||
const std::map<std::pair<int, int>, TileRoad>& getTileRoads() const { return m_tileRoads; }
|
||||
const TileRoad* getRoadAt(int tx, int ty) const;
|
||||
bool hasRoadAt(int tx, int ty) const;
|
||||
void addRoad(int tx, int ty, int playerId, const QString &roadTypeId = "dirt", Matrix2D<TerrainPoint> *mapPoints = nullptr, int numPoints05 = 0);
|
||||
void removeRoad(int tx, int ty);
|
||||
void updateRoadConnections(int tx, int ty);
|
||||
float getRoadSpeedMultiplierBetween(int fromX, int fromY, int toX, int toY) const;
|
||||
|
||||
float getTickDuration() const { return m_movementTickDuration; }
|
||||
void setTickDuration(float d) { m_movementTickDuration = std::max(1.0f, d); }
|
||||
float getTickTimer() const { return m_tickTimer; }
|
||||
@@ -96,6 +139,10 @@ public:
|
||||
float getSimSpeed() const { return m_simSpeedMultiplier; }
|
||||
void setSimSpeed(float s) { m_simSpeedMultiplier = std::max(0.2f, std::min(10.0f, s)); }
|
||||
|
||||
int getMaxActionQueueSize() const { return m_maxActionQueueSize; }
|
||||
bool isActionQueueingAllowed() const { return m_allowActionQueueing; }
|
||||
bool isAutoQueueMoveWhenBusy() const { return m_autoQueueMoveWhenBusy; }
|
||||
|
||||
void addFloatingText(const QString &text, const QVector3D &worldPos, const QColor &color, float duration = 1.8f);
|
||||
|
||||
int getNextUnitId() { return ++m_nextUnitId; }
|
||||
@@ -110,10 +157,13 @@ private:
|
||||
|
||||
PlayerManager m_playerManager;
|
||||
UnitConfigManager m_unitConfigManager;
|
||||
BuildingConfigManager m_buildingConfigManager;
|
||||
RoadConfigManager m_roadConfigManager;
|
||||
std::vector<std::shared_ptr<Unit>> m_units;
|
||||
std::vector<City> m_cities;
|
||||
std::vector<FloatingText> m_floatingTexts;
|
||||
std::map<std::pair<int, int>, TileImprovement> m_tileImprovements;
|
||||
std::map<std::pair<int, int>, TileRoad> m_tileRoads;
|
||||
|
||||
int m_selectedUnitId;
|
||||
int m_activeCityViewId;
|
||||
@@ -123,6 +173,10 @@ private:
|
||||
float m_tickTimer;
|
||||
float m_simSpeedMultiplier; // 1.0x, 2.0x, etc.
|
||||
|
||||
int m_maxActionQueueSize;
|
||||
bool m_autoQueueMoveWhenBusy;
|
||||
bool m_allowActionQueueing;
|
||||
|
||||
int m_nextUnitId;
|
||||
int m_nextCityId;
|
||||
bool m_initialized;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 962 B |
Binary file not shown.
|
After Width: | Height: | Size: 940 B |
Binary file not shown.
|
After Width: | Height: | Size: 650 B |
Binary file not shown.
+1
-1
@@ -897,7 +897,7 @@ static const unsigned char qt_resource_struct[] = {
|
||||
0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,
|
||||
// :/shaders/simple.frag
|
||||
0x0,0x0,0x0,0x14,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x0,0x0,
|
||||
0x0,0x0,0x1,0xa1,0xbb,0xdb,0xc9,0xbe,
|
||||
0x0,0x0,0x1,0xa1,0xbb,0xeb,0xa5,0x52,
|
||||
// :/shaders/simple.vert
|
||||
0x0,0x0,0x0,0x30,0x0,0x0,0x0,0x0,0x0,0x1,0x0,0x0,0x31,0x1d,
|
||||
0x0,0x0,0x1,0xa1,0xba,0xb6,0x16,0x20,
|
||||
|
||||
Binary file not shown.
+949
-73
File diff suppressed because it is too large
Load Diff
@@ -56,6 +56,7 @@ protected:
|
||||
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
// City & Territory Overlays
|
||||
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void drawRoadsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
|
||||
@@ -134,6 +135,43 @@ protected:
|
||||
int cityId;
|
||||
};
|
||||
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;
|
||||
|
||||
void drawCityResourceMenu(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void onSlotLongPressTimeout();
|
||||
|
||||
// Slot Long Press & Resource Selection Menu State
|
||||
bool m_slotPressPending = false;
|
||||
bool m_slotLongPressTriggered = false;
|
||||
int m_pressedSlotTx = -1;
|
||||
int m_pressedSlotTy = -1;
|
||||
QPoint m_slotPressPos;
|
||||
QElapsedTimer m_slotPressElapsed;
|
||||
QTimer *m_slotLongPressTimer = nullptr;
|
||||
|
||||
bool m_resourceMenuOpen = false;
|
||||
int m_resourceMenuTx = -1;
|
||||
int m_resourceMenuTy = -1;
|
||||
QRect m_resourceMenuRect;
|
||||
QRect m_resourceMenuCloseBtnRect;
|
||||
|
||||
struct CityResourceChoiceHit {
|
||||
QRect rect;
|
||||
QString res;
|
||||
int amount;
|
||||
};
|
||||
std::vector<CityResourceChoiceHit> m_cityResourceChoiceHits;
|
||||
};
|
||||
|
||||
#endif // QSCENERENDERER_H
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,94 @@
|
||||
#include "roadconfig.h"
|
||||
#include "yamlparser.h"
|
||||
#include <QFileInfo>
|
||||
#include <QDebug>
|
||||
|
||||
RoadConfigManager::RoadConfigManager()
|
||||
: m_defaultRoadType("dirt"),
|
||||
m_buildDurationTicks(2),
|
||||
m_meanderAmplitude(0.045f),
|
||||
m_meanderSubdivisions(5)
|
||||
{
|
||||
// Provide built-in defaults
|
||||
RoadTypeConfig dirt;
|
||||
dirt.id = "dirt";
|
||||
dirt.name = "Dirt Road";
|
||||
dirt.speedMultiplier = 1.30f;
|
||||
dirt.colorStr = "#c49a62";
|
||||
dirt.borderColorStr = "#6b4b23";
|
||||
dirt.width = 3.5f;
|
||||
dirt.cost = 5;
|
||||
m_types["dirt"] = dirt;
|
||||
|
||||
RoadTypeConfig stone;
|
||||
stone.id = "stone";
|
||||
stone.name = "Stone Road";
|
||||
stone.speedMultiplier = 1.60f;
|
||||
stone.colorStr = "#9aa1a8";
|
||||
stone.borderColorStr = "#464b50";
|
||||
stone.width = 4.0f;
|
||||
stone.cost = 15;
|
||||
m_types["stone"] = stone;
|
||||
|
||||
RoadTypeConfig rail;
|
||||
rail.id = "rail";
|
||||
rail.name = "Railroad";
|
||||
rail.speedMultiplier = 2.50f;
|
||||
rail.colorStr = "#3a3c40";
|
||||
rail.borderColorStr = "#181a1c";
|
||||
rail.width = 4.5f;
|
||||
rail.cost = 30;
|
||||
m_types["rail"] = rail;
|
||||
}
|
||||
|
||||
bool RoadConfigManager::loadConfigFile(const QString &filePath)
|
||||
{
|
||||
YamlNode root = YamlParser::parseFile(filePath);
|
||||
if (root.children.isEmpty()) {
|
||||
qWarning() << "Failed to parse roads config YAML, using defaults:" << filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (root.hasChild("default_road_type")) {
|
||||
m_defaultRoadType = root["default_road_type"].asString("dirt");
|
||||
}
|
||||
if (root.hasChild("build_duration_ticks")) {
|
||||
m_buildDurationTicks = root["build_duration_ticks"].asInt(2);
|
||||
}
|
||||
if (root.hasChild("meander_amplitude")) {
|
||||
m_meanderAmplitude = root["meander_amplitude"].asFloat(0.045f);
|
||||
}
|
||||
if (root.hasChild("meander_subdivisions")) {
|
||||
m_meanderSubdivisions = root["meander_subdivisions"].asInt(5);
|
||||
}
|
||||
|
||||
if (root.hasChild("types")) {
|
||||
const YamlNode &typesNode = root["types"];
|
||||
for (auto it = typesNode.children.begin(); it != typesNode.children.end(); ++it) {
|
||||
RoadTypeConfig rCfg;
|
||||
rCfg.id = it.value().hasChild("id") ? it.value()["id"].asString() : it.key();
|
||||
rCfg.name = it.value().hasChild("name") ? it.value()["name"].asString() : rCfg.id;
|
||||
rCfg.speedMultiplier = it.value().hasChild("speed_multiplier") ? it.value()["speed_multiplier"].asFloat(1.30f) : 1.30f;
|
||||
rCfg.colorStr = it.value().hasChild("color") ? it.value()["color"].asString("#c49a62") : "#c49a62";
|
||||
rCfg.borderColorStr = it.value().hasChild("border_color") ? it.value()["border_color"].asString("#6b4b23") : "#6b4b23";
|
||||
rCfg.width = it.value().hasChild("width") ? it.value()["width"].asFloat(3.5f) : 3.5f;
|
||||
rCfg.cost = it.value().hasChild("cost") ? it.value()["cost"].asInt(5) : 5;
|
||||
m_types[rCfg.id] = rCfg;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const RoadTypeConfig* RoadConfigManager::getRoadType(const QString &id) const
|
||||
{
|
||||
auto it = m_types.find(id);
|
||||
if (it != m_types.end()) {
|
||||
return &it.value();
|
||||
}
|
||||
auto defIt = m_types.find(m_defaultRoadType);
|
||||
if (defIt != m_types.end()) {
|
||||
return &defIt.value();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef ROADCONFIG_H
|
||||
#define ROADCONFIG_H
|
||||
|
||||
#include <QString>
|
||||
#include <QMap>
|
||||
#include <QColor>
|
||||
|
||||
struct RoadTypeConfig {
|
||||
QString id = "dirt";
|
||||
QString name = "Dirt Road";
|
||||
float speedMultiplier = 1.30f;
|
||||
QString colorStr = "#c49a62";
|
||||
QString borderColorStr = "#6b4b23";
|
||||
float width = 3.5f;
|
||||
int cost = 5;
|
||||
|
||||
QColor getColor() const { return QColor(colorStr); }
|
||||
QColor getBorderColor() const { return QColor(borderColorStr); }
|
||||
};
|
||||
|
||||
class RoadConfigManager {
|
||||
public:
|
||||
RoadConfigManager();
|
||||
|
||||
bool loadConfigFile(const QString &filePath);
|
||||
const RoadTypeConfig* getRoadType(const QString &id) const;
|
||||
const QMap<QString, RoadTypeConfig>& getAllRoadTypes() const { return m_types; }
|
||||
|
||||
QString defaultRoadType() const { return m_defaultRoadType; }
|
||||
int buildDurationTicks() const { return m_buildDurationTicks; }
|
||||
float meanderAmplitude() const { return m_meanderAmplitude; }
|
||||
int meanderSubdivisions() const { return m_meanderSubdivisions; }
|
||||
|
||||
private:
|
||||
QString m_defaultRoadType;
|
||||
int m_buildDurationTicks;
|
||||
float m_meanderAmplitude;
|
||||
int m_meanderSubdivisions;
|
||||
QMap<QString, RoadTypeConfig> m_types;
|
||||
};
|
||||
|
||||
#endif // ROADCONFIG_H
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "unitconfig.h"
|
||||
#include "pathfinder.h"
|
||||
#include "yamlparser.h"
|
||||
#include "gamemanager.h"
|
||||
#include <QDebug>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
@@ -12,8 +13,9 @@ TerrainMovementConfig::TerrainMovementConfig()
|
||||
snowHeightKm(3.1f),
|
||||
polarSnowLineEnabled(true),
|
||||
polarSnowLineBaseKm(2.5f),
|
||||
polarSnowFactorStart(0.88f),
|
||||
polarSnowFactorEnd(0.98f),
|
||||
polarSnowFactorStart(0.40f),
|
||||
polarSnowFactorEnd(0.85f),
|
||||
polarLandIceThreshold(0.72f),
|
||||
uphillSlopeMultiplier(4.0f),
|
||||
downhillSlopeBonusMultiplier(2.0f),
|
||||
minStepCost(0.25f),
|
||||
@@ -77,6 +79,9 @@ bool TerrainMovementManager::loadConfig(const QString &filePath)
|
||||
if (root.hasChild("polar_snow_factor_end")) {
|
||||
m_config.polarSnowFactorEnd = root["polar_snow_factor_end"].asFloat(0.85f);
|
||||
}
|
||||
if (root.hasChild("polar_land_ice_threshold")) {
|
||||
m_config.polarLandIceThreshold = root["polar_land_ice_threshold"].asFloat(0.72f);
|
||||
}
|
||||
|
||||
if (root.hasChild("uphill_slope_multiplier")) {
|
||||
m_config.uphillSlopeMultiplier = root["uphill_slope_multiplier"].asFloat(4.0f);
|
||||
@@ -164,6 +169,13 @@ QString TerrainMovementManager::getTerrainType(int tx, int ty, Matrix2D<TerrainP
|
||||
return "sneeuw";
|
||||
}
|
||||
}
|
||||
if (distFromCenter >= m_config.polarLandIceThreshold) {
|
||||
float tIce = std::max(0.0f, std::min(1.0f, (distFromCenter - m_config.polarLandIceThreshold) / 0.20f));
|
||||
float polarLandIce = tIce * tIce * (3.0f - 2.0f * tIce);
|
||||
if (polarLandIce > 0.5f) {
|
||||
return "sneeuw";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Rocky mountain
|
||||
@@ -289,15 +301,22 @@ float TerrainMovementManager::calculateStepCost(
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
float cost = baseCost;
|
||||
if (deltaH > 0.001f) {
|
||||
// Uphill penalty: going uphill increases movement cost
|
||||
float penalty = deltaH * m_config.uphillSlopeMultiplier;
|
||||
return baseCost + penalty;
|
||||
cost = baseCost + penalty;
|
||||
} else if (deltaH < -0.001f) {
|
||||
// Downhill bonus: going downhill decreases movement cost (negative penalty)
|
||||
float bonus = (-deltaH) * m_config.downhillSlopeBonusMultiplier;
|
||||
return std::max(m_config.minStepCost, baseCost - bonus);
|
||||
cost = std::max(m_config.minStepCost, baseCost - bonus);
|
||||
}
|
||||
|
||||
return baseCost;
|
||||
// Road speed bonus: if traveling between connected road tiles, movement speed increases!
|
||||
float roadMult = GameManager::instance().getRoadSpeedMultiplierBetween(fromX, fromY, toX, toY);
|
||||
if (roadMult > 1.0f) {
|
||||
cost /= roadMult;
|
||||
}
|
||||
|
||||
return std::max(0.1f, cost);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ struct TerrainMovementConfig {
|
||||
float polarSnowLineBaseKm;
|
||||
float polarSnowFactorStart;
|
||||
float polarSnowFactorEnd;
|
||||
float polarLandIceThreshold;
|
||||
|
||||
float uphillSlopeMultiplier;
|
||||
float downhillSlopeBonusMultiplier;
|
||||
|
||||
Binary file not shown.
@@ -49,6 +49,12 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir)
|
||||
m_cityConfig.initialNationalResources[it.key()] = it.value().asInt(0);
|
||||
}
|
||||
}
|
||||
|
||||
m_cityConfig.slotLongPressDurationMs = cityRoot["slot_long_press_duration_ms"].asInt(400);
|
||||
|
||||
if (cityRoot.hasChild("resource_priority") && !cityRoot["resource_priority"].listValues.isEmpty()) {
|
||||
m_cityConfig.resourcePriority = cityRoot["resource_priority"].listValues;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Load terrain_yields.yaml
|
||||
|
||||
@@ -35,6 +35,9 @@ struct CityConfig {
|
||||
int tileCultureCost;
|
||||
float foodToInhabitantsRatio;
|
||||
|
||||
int slotLongPressDurationMs;
|
||||
QStringList resourcePriority;
|
||||
|
||||
QMap<QString, int> baseYields;
|
||||
QMap<QString, int> initialNationalResources;
|
||||
|
||||
@@ -43,7 +46,9 @@ struct CityConfig {
|
||||
initialBorderRadius(1), maxWorkableRadius(3), defaultTileSlots(1),
|
||||
cityViewZoom(4.8f),
|
||||
baseResourceYieldPerTick(500), tileCultureCost(10000),
|
||||
foodToInhabitantsRatio(1.0f)
|
||||
foodToInhabitantsRatio(1.0f),
|
||||
slotLongPressDurationMs(400),
|
||||
resourcePriority({"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"})
|
||||
{
|
||||
baseYields["voedsel"] = 500;
|
||||
baseYields["arbeid"] = 500;
|
||||
|
||||
Binary file not shown.
+6
-2
@@ -23,6 +23,7 @@ SOURCES += main.cpp \
|
||||
player.cpp \
|
||||
yamlparser.cpp \
|
||||
unitconfig.cpp \
|
||||
buildingconfig.cpp \
|
||||
pathfinder.cpp \
|
||||
unit.cpp \
|
||||
city.cpp \
|
||||
@@ -30,7 +31,8 @@ SOURCES += main.cpp \
|
||||
gamemanager.cpp \
|
||||
unitrenderer.cpp \
|
||||
localization.cpp \
|
||||
terrainmovementmanager.cpp
|
||||
terrainmovementmanager.cpp \
|
||||
roadconfig.cpp
|
||||
|
||||
HEADERS += mainwindow.h \
|
||||
qscenerenderer.h \
|
||||
@@ -43,6 +45,7 @@ HEADERS += mainwindow.h \
|
||||
player.h \
|
||||
yamlparser.h \
|
||||
unitconfig.h \
|
||||
buildingconfig.h \
|
||||
pathfinder.h \
|
||||
unit.h \
|
||||
city.h \
|
||||
@@ -50,7 +53,8 @@ HEADERS += mainwindow.h \
|
||||
gamemanager.h \
|
||||
unitrenderer.h \
|
||||
localization.h \
|
||||
terrainmovementmanager.h
|
||||
terrainmovementmanager.h \
|
||||
roadconfig.h
|
||||
|
||||
FORMS += mainwindow.ui
|
||||
|
||||
|
||||
@@ -35,12 +35,74 @@ float Unit::getTerrainHeight(float worldX, float worldY, Matrix2D<TerrainPoint>
|
||||
return (h / 255.0f) * 3.5f + 1.0f;
|
||||
}
|
||||
|
||||
bool Unit::queueAction(const UnitAction &action)
|
||||
{
|
||||
if ((int)actionQueue.size() >= maxQueueSize) {
|
||||
return false;
|
||||
}
|
||||
actionQueue.push_back(action);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Unit::clearActionQueue()
|
||||
{
|
||||
actionQueue.clear();
|
||||
}
|
||||
|
||||
bool Unit::hasQueuedActionType(UnitActionType type) const
|
||||
{
|
||||
for (const auto &act : actionQueue) {
|
||||
if (act.type == type) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Unit::removeQueuedActionsOfType(UnitActionType type)
|
||||
{
|
||||
for (auto it = actionQueue.begin(); it != actionQueue.end(); ) {
|
||||
if (it->type == type) {
|
||||
it = actionQueue.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnitAction Unit::popNextQueuedAction()
|
||||
{
|
||||
if (actionQueue.empty()) return UnitAction();
|
||||
UnitAction act = actionQueue.front();
|
||||
actionQueue.pop_front();
|
||||
return act;
|
||||
}
|
||||
|
||||
const UnitAction* Unit::peekNextQueuedAction() const
|
||||
{
|
||||
if (actionQueue.empty()) return nullptr;
|
||||
return &actionQueue.front();
|
||||
}
|
||||
|
||||
QString Unit::getQueuedActionSummary() const
|
||||
{
|
||||
if (actionQueue.empty()) return "";
|
||||
switch (actionQueue.front().type) {
|
||||
case UnitActionType::Move:
|
||||
return "Move";
|
||||
case UnitActionType::BuildImprovement:
|
||||
return "Build Farm";
|
||||
case UnitActionType::BuildCity:
|
||||
return "Found City";
|
||||
case UnitActionType::BuildRoad:
|
||||
return "Build Road";
|
||||
case UnitActionType::Attack:
|
||||
return "Attack";
|
||||
default:
|
||||
return "Action";
|
||||
}
|
||||
}
|
||||
|
||||
void Unit::setPath(const std::vector<std::pair<int, int>> &newPath)
|
||||
{
|
||||
if (state == UnitState::Building) {
|
||||
cancelBuilding();
|
||||
}
|
||||
|
||||
path = newPath;
|
||||
pathIndex = 0;
|
||||
moveProgress = 0.0f;
|
||||
@@ -60,6 +122,7 @@ void Unit::stopMovement()
|
||||
pathIndex = 0;
|
||||
moveProgress = 0.0f;
|
||||
state = UnitState::Idle;
|
||||
clearActionQueue();
|
||||
}
|
||||
|
||||
void Unit::startBuilding(const QString &impId, float duration)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QString>
|
||||
#include <QVector3D>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <utility>
|
||||
#include "unitconfig.h"
|
||||
#include "terrain.h"
|
||||
@@ -16,6 +17,32 @@ enum class UnitState {
|
||||
Building
|
||||
};
|
||||
|
||||
enum class UnitActionType {
|
||||
None,
|
||||
Move,
|
||||
BuildImprovement,
|
||||
BuildCity,
|
||||
BuildRoad,
|
||||
Attack
|
||||
};
|
||||
|
||||
struct UnitAction {
|
||||
UnitActionType type = UnitActionType::None;
|
||||
int targetX = -1;
|
||||
int targetY = -1;
|
||||
int targetUnitId = -1;
|
||||
QString extraData = ""; // e.g. improvementId ("farm")
|
||||
|
||||
UnitAction() = default;
|
||||
UnitAction(UnitActionType t) : type(t) {}
|
||||
UnitAction(UnitActionType t, int x, int y, const QString &data = "")
|
||||
: type(t), targetX(x), targetY(y), extraData(data) {}
|
||||
UnitAction(UnitActionType t, int targetId)
|
||||
: type(t), targetUnitId(targetId) {}
|
||||
|
||||
bool isValid() const { return type != UnitActionType::None; }
|
||||
};
|
||||
|
||||
class Unit {
|
||||
public:
|
||||
Unit(int uId, int pId, const UnitConfig &cfg, int startX, int startY, float groundZ);
|
||||
@@ -38,6 +65,19 @@ public:
|
||||
int targetTileX;
|
||||
int targetTileY;
|
||||
|
||||
// Action Queue
|
||||
std::deque<UnitAction> actionQueue;
|
||||
int maxQueueSize = 5;
|
||||
|
||||
bool queueAction(const UnitAction &action);
|
||||
void clearActionQueue();
|
||||
bool hasQueuedAction() const { return !actionQueue.empty(); }
|
||||
bool hasQueuedActionType(UnitActionType type) const;
|
||||
void removeQueuedActionsOfType(UnitActionType type);
|
||||
UnitAction popNextQueuedAction();
|
||||
const UnitAction* peekNextQueuedAction() const;
|
||||
QString getQueuedActionSummary() const;
|
||||
|
||||
// Attack cooldown / flash
|
||||
float attackFlashTimer;
|
||||
|
||||
@@ -54,6 +94,7 @@ public:
|
||||
void startBuilding(const QString &impId, float duration);
|
||||
void cancelBuilding();
|
||||
bool isBuilding() const { return state == UnitState::Building; }
|
||||
bool isBusy() const { return state == UnitState::Building; }
|
||||
|
||||
bool canAttack(const Unit &other) const;
|
||||
void performAttack(Unit &target);
|
||||
|
||||
@@ -42,6 +42,13 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
|
||||
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.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.attackRange = root.hasChild("attack_range") ? root["attack_range"].asInt(1) : 1;
|
||||
cfg.buildCharges = root.hasChild("build_charges") ? root["build_charges"].asInt(-1) : -1;
|
||||
|
||||
+4
-1
@@ -39,6 +39,7 @@ struct UnitConfig {
|
||||
int health;
|
||||
float movementSpeed;
|
||||
int cost;
|
||||
int laborCost;
|
||||
int attackDamage;
|
||||
int attackRange; // 1 for melee, >1 for ranged
|
||||
int buildCharges;
|
||||
@@ -52,7 +53,7 @@ struct UnitConfig {
|
||||
UnitConfig()
|
||||
: id(""), name(""), category(UnitCategory::Builder),
|
||||
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("") {}
|
||||
|
||||
bool isRanged() const { return attackRange > 1; }
|
||||
@@ -62,6 +63,8 @@ struct UnitConfig {
|
||||
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"); }
|
||||
bool canBuildRoad() const { return actions.contains("build_road") || actions.contains("weg_bouwen"); }
|
||||
bool isBuilder() const { return canBuildFarm() || canBuildRoad(); }
|
||||
|
||||
float getActionDuration(const QString &actionName, float fallback = 10.0f) const {
|
||||
if (actionSpeeds.contains(actionName)) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user