Compare commits

...
6 Commits
22 changed files with 1659 additions and 313 deletions
+3 -3
View File
@@ -24,13 +24,13 @@ This document serves as the primary technical specification, architecture refere
> - **Key-Based Lookup:** Code must reference screen texts exclusively by localization keys (e.g. `localization.get("ui.button.found_city")` or equivalent lookup) resolving to the external file. > - **Key-Based Lookup:** Code must reference screen texts exclusively by localization keys (e.g. `localization.get("ui.button.found_city")` or equivalent lookup) resolving to the external file.
> - Whenever new on-screen text is introduced, add its English translation entry to the external string file first before referencing its key in code. > - Whenever new on-screen text is introduced, add its English translation entry to the external string file first before referencing its key in code.
### 0.3 Session Git Branching & No-Build Directive ### 0.3 Session Git Branching & Build Restrictions
> [!IMPORTANT] > [!IMPORTANT]
> **Strict Agent Directive:** Every session must adhere to an isolated Git workflow and never build the project. > **Strict Agent Directive:** Every session must adhere to an isolated Git workflow and never build without permission.
> - **Dedicated Session Branch:** Every session MUST work on its own git branch. > - **Dedicated Session Branch:** Every session MUST work on its own git branch.
> - **Automatic Branch Creation:** If a session does not have its own branch yet upon starting, the agent MUST generate an appropriate branch name (e.g. `session/<topic-or-task>`) and check it out immediately (`git checkout -b <branch-name>`). > - **Automatic Branch Creation:** If a session does not have its own branch yet upon starting, the agent MUST generate an appropriate branch name (e.g. `session/<topic-or-task>`) and check it out immediately (`git checkout -b <branch-name>`).
> - **Do NOT Build the Application:** The agent MUST NOT compile or build the application (never run `make`, `qmake`, `cmake`, `ninja`, compiler invocations, or build scripts). Compiling and running the application is strictly left to the user. > - **Do NOT Build Without Explicit Permission:** The agent MUST NOT compile or build the application (never run `make`, `qmake`, `cmake`, `ninja`, compiler invocations, or build scripts) unless explicit permission has been granted by the user.
> - **Immediate Commits:** After making any change/edit to the project, the agent MUST immediately commit the changes to its current session branch with a clear and concise commit message (`git add <files>` and `git commit -m "..."`). > - **Immediate Commits:** After making any change/edit to the project, the agent MUST immediately commit the changes to its current session branch with a clear and concise commit message (`git add <files>` and `git commit -m "..."`).
--- ---
+243
View File
@@ -0,0 +1,243 @@
#include "actionneededmanager.h"
#include "yamlparser.h"
#include "unit.h"
#include "city.h"
#include <algorithm>
#include <set>
#include <QDebug>
ActionNeededManager::ActionNeededManager()
: m_elapsedGameTime(0.0)
{
}
bool ActionNeededManager::loadConfigFile(const QString &filePath)
{
YamlNode root = YamlParser::parseFile(filePath);
if (root.children.isEmpty()) {
qWarning() << "Failed to parse action needed YAML, using defaults:" << filePath;
return false;
}
const YamlNode &node = root.hasChild("action_needed") ? root["action_needed"] : root;
if (node.hasChild("enabled")) {
m_config.enabled = node["enabled"].asBool(true);
}
if (node.hasChild("track_idle_units")) {
m_config.trackIdleUnits = node["track_idle_units"].asBool(true);
}
if (node.hasChild("track_city_no_production")) {
m_config.trackCityNoProduction = node["track_city_no_production"].asBool(true);
}
if (node.hasChild("track_city_free_population")) {
m_config.trackCityFreePopulation = node["track_city_free_population"].asBool(true);
}
if (node.hasChild("max_visible_items")) {
m_config.maxVisibleItems = node["max_visible_items"].asInt(8);
}
if (node.hasChild("item_width")) {
m_config.itemWidth = node["item_width"].asInt(155);
}
if (node.hasChild("item_height")) {
m_config.itemHeight = node["item_height"].asInt(42);
}
if (node.hasChild("bottom_offset")) {
m_config.bottomOffset = node["bottom_offset"].asInt(16);
}
if (node.hasChild("item_spacing")) {
m_config.itemSpacing = node["item_spacing"].asInt(8);
}
if (node.hasChild("border_radius")) {
m_config.borderRadius = node["border_radius"].asInt(6);
}
if (node.hasChild("pulse_animation")) {
m_config.pulseAnimation = node["pulse_animation"].asBool(true);
}
if (node.hasChild("pulse_speed")) {
m_config.pulseSpeed = node["pulse_speed"].asFloat(3.0f);
}
if (node.hasChild("unit_idle_color")) {
QString hex = node["unit_idle_color"].asString("#3CA0FF");
m_config.unitIdleColor = QColor(hex);
}
if (node.hasChild("city_no_prod_color")) {
QString hex = node["city_no_prod_color"].asString("#FFA028");
m_config.cityNoProdColor = QColor(hex);
}
if (node.hasChild("city_free_pop_color")) {
QString hex = node["city_free_pop_color"].asString("#46DC82");
m_config.cityFreePopColor = QColor(hex);
}
if (node.hasChild("card_bg_color")) {
QString hex = node["card_bg_color"].asString("#121822");
m_config.cardBgColor = QColor(hex);
}
if (node.hasChild("card_bg_alpha")) {
m_config.cardBgAlpha = node["card_bg_alpha"].asInt(235);
}
m_config.cardBgColor.setAlpha(m_config.cardBgAlpha);
qDebug() << "ActionNeededManager config loaded from" << filePath;
return true;
}
void ActionNeededManager::update(float deltaTime, int activePlayerId,
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities)
{
if (!m_config.enabled) return;
m_elapsedGameTime += static_cast<double>(deltaTime);
std::set<QString> currentConditionKeys;
// 1. Idle Units waiting for action
if (m_config.trackIdleUnits) {
for (const auto &u : units) {
if (!u || u->isDead() || u->playerId != activePlayerId) continue;
bool isIdle = (u->state == UnitState::Idle)
&& (u->path.empty() || u->pathIndex >= u->path.size())
&& !u->hasQueuedAction()
&& !u->hasRoadRoute()
&& !u->isBuilding();
if (isIdle) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::UnitIdle)).arg(u->id);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::UnitIdle && item.entityId == u->id;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::UnitIdle;
newItem.entityId = u->id;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.dismissed = false;
m_items.push_back(newItem);
}
}
}
}
// 2. Cities waiting for action
for (const auto &c : cities) {
if (c.playerId != activePlayerId) continue;
// a) City is not building anything
if (m_config.trackCityNoProduction) {
bool noProd = !c.hasActiveProject();
if (noProd) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::CityNoProduction)).arg(c.id);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::CityNoProduction && item.entityId == c.id;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::CityNoProduction;
newItem.entityId = c.id;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.dismissed = false;
m_items.push_back(newItem);
}
}
}
// b) City has free / unassigned population
if (m_config.trackCityFreePopulation) {
int freePop = c.getAvailablePop();
if (freePop > 0) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::CityFreePopulation)).arg(c.id);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::CityFreePopulation && item.entityId == c.id;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::CityFreePopulation;
newItem.entityId = c.id;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.auxValue = freePop;
newItem.dismissed = false;
m_items.push_back(newItem);
} else {
// If free population increased since it was dismissed, reactivate it!
if (freePop > it->dismissedAuxValue) {
it->dismissed = false;
}
it->auxValue = freePop;
}
}
}
}
// 3. Problem resolved -> remove from queue!
m_items.erase(std::remove_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return currentConditionKeys.find(item.getKey()) == currentConditionKeys.end();
}), m_items.end());
}
std::vector<ActionItem> ActionNeededManager::getVisibleItems() const
{
std::vector<ActionItem> visible;
visible.reserve(m_items.size());
for (const auto &item : m_items) {
if (!item.dismissed) {
visible.push_back(item);
}
}
// Sort in order of oldest first (firstDetectedTime ASC)
std::sort(visible.begin(), visible.end(), [](const ActionItem &a, const ActionItem &b) {
if (a.firstDetectedTime != b.firstDetectedTime)
return a.firstDetectedTime < b.firstDetectedTime;
return a.entityId < b.entityId;
});
return visible;
}
int ActionNeededManager::getVisibleCount() const
{
int count = 0;
for (const auto &item : m_items) {
if (!item.dismissed) {
count++;
}
}
return count;
}
void ActionNeededManager::dismissItem(ActionNeededType type, int entityId)
{
for (auto &item : m_items) {
if (item.type == type && item.entityId == entityId) {
item.dismissed = true;
item.dismissedAuxValue = item.auxValue;
break;
}
}
}
void ActionNeededManager::resetDismissals()
{
for (auto &item : m_items) {
item.dismissed = false;
item.dismissedAuxValue = 0;
}
}
void ActionNeededManager::reset()
{
m_items.clear();
m_elapsedGameTime = 0.0;
}
+79
View File
@@ -0,0 +1,79 @@
#ifndef ACTIONNEEDEDMANAGER_H
#define ACTIONNEEDEDMANAGER_H
#include <QString>
#include <QColor>
#include <vector>
#include <memory>
class Unit;
class City;
enum class ActionNeededType {
UnitIdle = 0,
CityNoProduction = 1,
CityFreePopulation = 2
};
struct ActionItem {
ActionNeededType type = ActionNeededType::UnitIdle;
int entityId = -1;
double firstDetectedTime = 0.0; // Monotonic game time in seconds
int auxValue = 0; // e.g. free population count
bool dismissed = false;
int dismissedAuxValue = 0; // Value at the time of dismissal
QString getKey() const {
return QString("%1_%2").arg(static_cast<int>(type)).arg(entityId);
}
};
struct ActionNeededConfig {
bool enabled = true;
bool trackIdleUnits = true;
bool trackCityNoProduction = true;
bool trackCityFreePopulation = true;
int maxVisibleItems = 8;
int itemWidth = 155;
int itemHeight = 42;
int bottomOffset = 16;
int itemSpacing = 8;
int borderRadius = 6;
bool pulseAnimation = true;
float pulseSpeed = 3.0f;
QColor unitIdleColor = QColor(60, 160, 255);
QColor cityNoProdColor = QColor(255, 160, 40);
QColor cityFreePopColor = QColor(70, 220, 130);
QColor cardBgColor = QColor(18, 24, 34);
int cardBgAlpha = 235;
};
class ActionNeededManager {
public:
ActionNeededManager();
bool loadConfigFile(const QString &filePath);
const ActionNeededConfig& config() const { return m_config; }
void update(float deltaTime, int activePlayerId,
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities);
std::vector<ActionItem> getVisibleItems() const;
int getVisibleCount() const;
void dismissItem(ActionNeededType type, int entityId);
void resetDismissals();
double getElapsedGameTime() const { return m_elapsedGameTime; }
void reset();
private:
ActionNeededConfig m_config;
std::vector<ActionItem> m_items;
double m_elapsedGameTime;
};
#endif // ACTIONNEEDEDMANAGER_H
+75 -1
View File
@@ -55,6 +55,7 @@ public:
int accumulatedLabor = 0; // Local resource (Arbeid) int accumulatedLabor = 0; // Local resource (Arbeid)
CityProject currentProject; CityProject currentProject;
std::vector<CityProject> projectQueue;
std::vector<QString> completedBuildings; // IDs of completed buildings in the city 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
@@ -77,6 +78,34 @@ public:
return true; return true;
} }
bool isBuildingQueuedOrBuilt(const QString &buildingId) const {
if (hasBuilding(buildingId)) return true;
if (currentProject.isValid() && currentProject.type == CityProjectType::Building && currentProject.id == buildingId) return true;
for (const auto &q : projectQueue) {
if (q.type == CityProjectType::Building && q.id == buildingId) return true;
}
return false;
}
int getQueuedBuildingPosition(const QString &buildingId) const {
for (size_t i = 0; i < projectQueue.size(); ++i) {
if (projectQueue[i].type == CityProjectType::Building && projectQueue[i].id == buildingId) {
return (int)i + 1;
}
}
return 0;
}
int getQueuedUnitCount(const QString &unitId) const {
int count = 0;
for (const auto &q : projectQueue) {
if (q.type == CityProjectType::Unit && q.id == unitId) {
count++;
}
}
return count;
}
bool hasActiveProject() const { bool hasActiveProject() const {
return currentProject.isValid(); return currentProject.isValid();
} }
@@ -90,9 +119,54 @@ public:
currentProject.district = district.isEmpty() ? "center" : district; currentProject.district = district.isEmpty() ? "center" : district;
} }
void cancelProject() { bool queueProject(CityProjectType type, const QString &projId, const QString &projName, int cost, const QString &district = "center", int maxQueueSize = 6) {
if (!hasActiveProject()) {
startProject(type, projId, projName, cost, district);
return true;
}
if ((int)projectQueue.size() >= maxQueueSize) {
return false;
}
CityProject q;
q.type = type;
q.id = projId;
q.name = projName;
q.laborCost = std::max(1, cost);
q.currentLabor = 0;
q.district = district.isEmpty() ? "center" : district;
projectQueue.push_back(q);
return true;
}
void cancelActiveProject() {
if (!projectQueue.empty()) {
currentProject = projectQueue.front();
projectQueue.erase(projectQueue.begin());
} else {
currentProject = CityProject(); currentProject = CityProject();
} }
}
void cancelProject() {
cancelActiveProject();
}
bool removeQueuedProject(int index) {
if (index >= 0 && index < (int)projectQueue.size()) {
projectQueue.erase(projectQueue.begin() + index);
return true;
}
return false;
}
void advanceQueue() {
if (!projectQueue.empty()) {
currentProject = projectQueue.front();
projectQueue.erase(projectQueue.begin());
} else {
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;
+23
View File
@@ -0,0 +1,23 @@
# Configuration for the Action Needed Queue (Bottom HUD notification bar)
action_needed:
enabled: true
track_idle_units: true
track_city_no_production: true
track_city_free_population: true
max_visible_items: 8
# Visual & Layout parameters
item_width: 155
item_height: 42
bottom_offset: 16
item_spacing: 8
border_radius: 6
pulse_animation: true
pulse_speed: 3.0
# Colors
unit_idle_color: "#3CA0FF"
city_no_prod_color: "#FFA028"
city_free_pop_color: "#46DC82"
card_bg_color: "#121822"
card_bg_alpha: 235
+1
View File
@@ -14,6 +14,7 @@ tile_culture_cost: 10000
food_to_inhabitants_ratio: 1.0 food_to_inhabitants_ratio: 1.0
default_unit_labor_cost: 1000 default_unit_labor_cost: 1000
unit_labor_cost_multiplier: 20 unit_labor_cost_multiplier: 20
max_production_queue_size: 6
# Base yield breakdown per resource (default 500 per tick) # Base yield breakdown per resource (default 500 per tick)
base_yields: base_yields:
+3
View File
@@ -12,6 +12,7 @@ types:
color: "#c49a62" color: "#c49a62"
border_color: "#6b4b23" border_color: "#6b4b23"
width: 3.5 width: 3.5
world_width: 0.11
cost: 5 cost: 5
stone: stone:
id: "stone" id: "stone"
@@ -20,6 +21,7 @@ types:
color: "#9aa1a8" color: "#9aa1a8"
border_color: "#464b50" border_color: "#464b50"
width: 4.0 width: 4.0
world_width: 0.13
cost: 15 cost: 15
rail: rail:
id: "rail" id: "rail"
@@ -28,4 +30,5 @@ types:
color: "#3a3c40" color: "#3a3c40"
border_color: "#181a1c" border_color: "#181a1c"
width: 4.5 width: 4.5
world_width: 0.15
cost: 30 cost: 30
+30 -5
View File
@@ -102,7 +102,7 @@ ui:
tip_build_farm: "Build Farm (F)" tip_build_farm: "Build Farm (F)"
tip_farm_building: "Busy building... %1%" tip_farm_building: "Busy building... %1%"
tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)" tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)"
tip_build_road: "Build Road (R)" tip_build_road: "Build Road (R) - Click target tile to build route"
tip_road_building: "Busy building road... %1%" tip_road_building: "Busy building road... %1%"
tip_road_queued: "Build Road (Queued on arrival - click to cancel)" tip_road_queued: "Build Road (Queued on arrival - click to cancel)"
road_started: "Constructing %1... (%2 ticks)" road_started: "Constructing %1... (%2 ticks)"
@@ -110,6 +110,11 @@ ui:
road_queued: "Build Road queued on arrival" road_queued: "Build Road queued on arrival"
road_already_here: "Tile already has a road!" road_already_here: "Tile already has a road!"
cannot_build_road_water: "Roads cannot be built on water!" cannot_build_road_water: "Roads cannot be built on water!"
road_route_started: "Building road route to (%1, %2)..."
road_route_completed: "Road route completed!"
road_route_cancelled: "Road route cancelled"
status_road_route: "Road Route: %1/%2"
btn_road_route: "Route (%1/%2)"
improvement: improvement:
farm: "Farm" farm: "Farm"
@@ -126,25 +131,35 @@ ui:
national_info: "National: Culture: %L1 | Science: %L2 | Gold: %L3 | Faith: %L4" national_info: "National: Culture: %L1 | Science: %L2 | Gold: %L3 | Faith: %L4"
help_text: "Click a slot to assign/free Pop | Hold a slot to choose resource | Buy tile with Culture." help_text: "Click a slot to assign/free Pop | Hold a slot to choose resource | Buy tile with Culture."
current_production: "CURRENT PRODUCTION" current_production: "CURRENT PRODUCTION"
production_panel_title: "CITY PRODUCTION"
no_project: "No active project" no_project: "No active project"
select_prompt: "Select a unit or building to construct" select_prompt: "Select an item below to construct"
ticks_remaining: "%1 ticks remaining (%2 / %3 Labor)" ticks_remaining: "%1 ticks remaining (%2 / %3 Labor)"
cancel_btn: "Cancel" cancel_btn: "Cancel"
queue_header: "QUEUE (%1 / %2)"
queue_empty: "Queue is empty"
districts_header: "DISTRICT: CITY CENTER" districts_header: "DISTRICT: CITY CENTER"
no_buildings: "No extensions built yet" no_buildings: "No extensions built yet"
choose_project: "CHOOSE CITY PROJECT" choose_project: "AVAILABLE PROJECTS"
tab_buildings: "Buildings & Extensions" tab_buildings: "Buildings"
tab_units: "Units" tab_units: "Units"
btn_build: "Build" btn_build: "Build"
btn_train: "Train" btn_train: "Train"
btn_queue: "+ Queue"
status_built: "BUILT" status_built: "BUILT"
status_in_progress: "ACTIVE" status_in_progress: "ACTIVE"
status_queued: "QUEUED #%1"
status_full: "FULL"
queue_full_float: "Production queue is full! (Max %1)"
labor_cost_label: "%1 Labor (%2 ticks)" labor_cost_label: "%1 Labor (%2 ticks)"
unit_trained: "%1: %2 trained!" unit_trained: "%1: %2 trained!"
building_completed: "%1: %2 completed in %3!" building_completed: "%1: %2 completed in %3!"
project_started: "%1: Started %2" project_started: "%1: Started %2"
project_cancelled: "%1: Project cancelled" project_queued: "%1: Added %2 to queue (#%3)"
project_cancelled: "%1: Cancelled %2"
queued_item_removed: "%1: Removed %2 from queue"
already_built: "%1 already built in this city!" already_built: "%1 already built in this city!"
already_queued: "%1 is already in the production queue!"
growth_float: "+%1 Inhabitants | +%2 Culture" growth_float: "+%1 Inhabitants | +%2 Culture"
not_enough_culture: "Not enough Culture! (%1 needed)" not_enough_culture: "Not enough Culture! (%1 needed)"
worker_freed: "Pop worker freed" worker_freed: "Pop worker freed"
@@ -165,3 +180,13 @@ ui:
kennis: "Science" kennis: "Science"
cultuur: "Culture" cultuur: "Culture"
geloof: "Faith" geloof: "Faith"
action_needed:
header_title: "ACTIONS"
header_tip: "Queue of items awaiting action (oldest first). Left-click to focus, Right-click to dismiss."
unit_idle_desc: "Needs orders"
city_no_production_desc: "No production"
city_free_pop_desc: "+%1 Free Pop"
dismiss_tip: "Right-click to dismiss"
more_items: "+%1"
+178 -8
View File
@@ -58,6 +58,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
QString roadsConfigPath = baseDir + "/config/roads.yaml"; QString roadsConfigPath = baseDir + "/config/roads.yaml";
m_roadConfigManager.loadConfigFile(roadsConfigPath); m_roadConfigManager.loadConfigFile(roadsConfigPath);
// Load action needed queue configuration
QString actionNeededConfigPath = baseDir + "/config/action_needed.yaml";
m_actionNeededManager.loadConfigFile(actionNeededConfigPath);
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
// Initialize player national resources from configuration // Initialize player national resources from configuration
@@ -133,11 +137,15 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
if (u->isBuilding() && u->buildProgress >= 1.0f) { if (u->isBuilding() && u->buildProgress >= 1.0f) {
completeUnitBuilding(*u, mapPoints, numPoints05); completeUnitBuilding(*u, mapPoints, numPoints05);
} }
if (!u->isDead() && u->state == UnitState::Idle && u->hasQueuedAction()) { if (!u->isDead() && u->state == UnitState::Idle) {
if (u->hasRoadRoute()) {
advanceRoadRoute(*u, mapPoints, numPoints05);
} else if (u->hasQueuedAction()) {
processUnitActionQueue(*u, mapPoints, numPoints05); processUnitActionQueue(*u, mapPoints, numPoints05);
} }
} }
} }
}
// Clean up dead units // Clean up dead units
for (auto it = m_units.begin(); it != m_units.end(); ) { for (auto it = m_units.begin(); it != m_units.end(); ) {
@@ -162,6 +170,9 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
++it; ++it;
} }
} }
// Update Action Needed Queue for active human player 1
m_actionNeededManager.update(deltaTime, 1, m_units, m_cities);
} }
Unit* GameManager::getSelectedUnit() Unit* GameManager::getSelectedUnit()
@@ -255,7 +266,7 @@ QMap<QString, int> GameManager::getTileYields(int tx, int ty, Matrix2D<TerrainPo
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
QStringList resources = cCfg.resourcePriority; QStringList resources = cCfg.resourcePriority;
if (resources.isEmpty()) { if (resources.isEmpty()) {
resources = {"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"}; resources = QStringList{"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"};
} }
QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05);
@@ -293,7 +304,7 @@ QString GameManager::getBestTileResource(int tx, int ty, Matrix2D<TerrainPoint>
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
QStringList priority = cCfg.resourcePriority; QStringList priority = cCfg.resourcePriority;
if (priority.isEmpty()) { if (priority.isEmpty()) {
priority = {"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"}; priority = QStringList{"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"};
} }
QMap<QString, int> yields = getTileYields(tx, ty, mapPoints, numPoints05); QMap<QString, int> yields = getTileYields(tx, ty, mapPoints, numPoints05);
@@ -455,31 +466,74 @@ bool GameManager::buyTileForCity(int cityId, int tx, int ty)
return true; return true;
} }
void GameManager::setCityProject(int cityId, CityProjectType type, const QString &id) void GameManager::queueCityProject(int cityId, CityProjectType type, const QString &id)
{ {
City *city = getCityById(cityId); City *city = getCityById(cityId);
if (!city) return; if (!city) return;
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
int maxQueue = cCfg.maxProductionQueueSize;
if (type == CityProjectType::Unit) { if (type == CityProjectType::Unit) {
const UnitConfig *uCfg = m_unitConfigManager.getConfig(id); const UnitConfig *uCfg = m_unitConfigManager.getConfig(id);
if (!uCfg) return; if (!uCfg) return;
if (!city->hasActiveProject()) {
city->startProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost); city->startProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost);
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2") QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
.arg(city->name).arg(uCfg->name); .arg(city->name).arg(uCfg->name);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f); addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
} else {
if ((int)city->projectQueue.size() >= maxQueue) {
QString msg = Localization::instance().get("ui.city.queue_full_float", "Production queue is full! (Max %1)").arg(maxQueue);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.0f);
return;
}
city->queueProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost, "center", maxQueue);
int pos = (int)city->projectQueue.size();
QString msg = Localization::instance().get("ui.city.project_queued", "%1: Added %2 to queue (#%3)")
.arg(city->name).arg(uCfg->name).arg(pos);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(100, 200, 255), 2.2f);
}
} else if (type == CityProjectType::Building) { } else if (type == CityProjectType::Building) {
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(id); const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(id);
if (!bCfg) return; if (!bCfg) return;
if (city->hasBuilding(id)) { if (city->hasBuilding(id)) {
QString msg = Localization::instance().get("ui.city.already_built", "%1 already built!").arg(bCfg->name); QString msg = Localization::instance().get("ui.city.already_built", "%1 already built in this city!").arg(bCfg->name);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f); addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f);
return; return;
} }
if (city->isBuildingQueuedOrBuilt(id)) {
QString msg = Localization::instance().get("ui.city.already_queued", "%1 is already in the production queue!").arg(bCfg->name);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 160, 60), 2.2f);
return;
}
if (!city->hasActiveProject()) {
city->startProject(CityProjectType::Building, id, bCfg->name, bCfg->laborCost, bCfg->district); city->startProject(CityProjectType::Building, id, bCfg->name, bCfg->laborCost, bCfg->district);
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2") QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
.arg(city->name).arg(bCfg->name); .arg(city->name).arg(bCfg->name);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f); addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
} else {
if ((int)city->projectQueue.size() >= maxQueue) {
QString msg = Localization::instance().get("ui.city.queue_full_float", "Production queue is full! (Max %1)").arg(maxQueue);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.0f);
return;
} }
city->queueProject(CityProjectType::Building, id, bCfg->name, bCfg->laborCost, bCfg->district, maxQueue);
int pos = (int)city->projectQueue.size();
QString msg = Localization::instance().get("ui.city.project_queued", "%1: Added %2 to queue (#%3)")
.arg(city->name).arg(bCfg->name).arg(pos);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(100, 200, 255), 2.2f);
}
}
}
void GameManager::setCityProject(int cityId, CityProjectType type, const QString &id)
{
queueCityProject(cityId, type, id);
} }
void GameManager::cancelCityProject(int cityId) void GameManager::cancelCityProject(int cityId)
@@ -487,9 +541,31 @@ void GameManager::cancelCityProject(int cityId)
City *city = getCityById(cityId); City *city = getCityById(cityId);
if (!city || !city->hasActiveProject()) return; if (!city || !city->hasActiveProject()) return;
QString msg = Localization::instance().get("ui.city.project_cancelled", "%1: Project cancelled").arg(city->name); QString cancelledName = city->currentProject.name;
city->cancelActiveProject();
QString msg = Localization::instance().get("ui.city.project_cancelled", "%1: Cancelled %2").arg(city->name).arg(cancelledName);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 180, 80), 2.0f); addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 180, 80), 2.0f);
city->cancelProject();
if (city->hasActiveProject()) {
QString nextMsg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
.arg(city->name).arg(city->currentProject.name);
addFloatingText(nextMsg, city->worldPos + QVector3D(0.0f, 0.0f, 1.4f), QColor(255, 230, 80), 2.5f);
}
}
void GameManager::removeQueuedCityProject(int cityId, int queueIndex)
{
City *city = getCityById(cityId);
if (!city) return;
if (queueIndex >= 0 && queueIndex < (int)city->projectQueue.size()) {
QString itemName = city->projectQueue[queueIndex].name;
city->removeQueuedProject(queueIndex);
QString msg = Localization::instance().get("ui.city.queued_item_removed", "%1: Removed %2 from queue")
.arg(city->name).arg(itemName);
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 170, 80), 2.0f);
}
} }
void GameManager::completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) void GameManager::completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
@@ -540,8 +616,16 @@ void GameManager::completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoi
} }
int excess = city.currentProject.currentLabor - city.currentProject.laborCost; int excess = city.currentProject.currentLabor - city.currentProject.laborCost;
city.advanceQueue();
if (city.hasActiveProject() && excess > 0) {
city.currentProject.currentLabor += excess;
if (city.currentProject.currentLabor >= city.currentProject.laborCost) {
completeCityProject(city, mapPoints, numPoints05);
}
} else {
city.accumulatedLabor += std::max(0, excess); 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)
@@ -590,6 +674,7 @@ void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2D<Terrain
if (path.size() > 1) { if (path.size() > 1) {
selected->clearActionQueue(); selected->clearActionQueue();
selected->clearRoadRoute();
selected->setPath(path); selected->setPath(path);
addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"), addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80)); selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80));
@@ -959,6 +1044,86 @@ void GameManager::commandBuildRoadSelected(Matrix2D<TerrainPoint> *mapPoints, in
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f); selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
} }
void GameManager::commandBuildRoadToTileSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
Unit *selected = getSelectedUnit();
if (!selected || !selected->config.canBuildRoad()) return;
if (!TerrainMovementManager::instance().isHexPassable(
targetX, targetY, 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;
}
// Pathfind from current tile to target tile
std::vector<std::pair<int, int>> route = Pathfinder::findPath(
selected->tileX, selected->tileY, targetX, targetY,
MovementDomain::Land, mapPoints, numPoints05, &selected->config
);
if (route.empty()) {
addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90));
return;
}
selected->clearActionQueue();
selected->startRoadRoute(route);
QString defaultType = m_roadConfigManager.defaultRoadType();
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType);
QString roadName = rCfg ? rCfg->name : "Road";
addFloatingText(Localization::instance().get("ui.unit.road_route_started", "Building road route to (%1, %2)...").arg(targetX).arg(targetY),
selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.2f);
advanceRoadRoute(*selected, mapPoints, numPoints05);
}
void GameManager::advanceRoadRoute(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{
if (!unit.hasRoadRoute()) return;
while (unit.roadRouteIndex < unit.roadRoute.size()) {
auto nextHex = unit.roadRoute[unit.roadRouteIndex];
int tx = nextHex.first;
int ty = nextHex.second;
// If unit is not on this tile yet, move to it!
if (unit.tileX != tx || unit.tileY != ty) {
unit.setPath({ {unit.tileX, unit.tileY}, {tx, ty} });
unit.state = UnitState::Moving;
return;
}
// Unit is on this tile! Does it already have a road?
if (hasRoadAt(tx, ty)) {
// Already has a road, skip to next tile along route
unit.advanceRoadRouteStep();
continue;
}
// Start building road on this tile
QString defaultType = m_roadConfigManager.defaultRoadType();
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType);
QString roadName = rCfg ? rCfg->name : "Road";
int ticks = m_roadConfigManager.buildDurationTicks();
float duration = (float)ticks * m_movementTickDuration;
unit.startBuilding(QString("road:%1").arg(defaultType), 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);
return;
}
// Finished entire route!
unit.clearRoadRoute();
QVector2D w = Pathfinder::hexToWorld(unit.tileX, unit.tileY);
float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05);
addFloatingText(Localization::instance().get("ui.unit.road_route_completed", "Road route completed!"),
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(100, 255, 140), 3.0f);
}
void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05) void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{ {
int tx = unit.tileX; int tx = unit.tileX;
@@ -981,6 +1146,11 @@ void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPo
QString roadName = rCfg ? rCfg->name : "Road"; QString roadName = rCfg ? rCfg->name : "Road";
addFloatingText(Localization::instance().get("ui.unit.road_completed", "%1 completed!").arg(roadName), 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); QVector3D(w.x(), w.y(), gz + 1.2f), QColor(240, 210, 100), 2.8f);
if (unit.hasRoadRoute()) {
unit.advanceRoadRouteStep();
advanceRoadRoute(unit, mapPoints, numPoints05);
}
return; return;
} }
+8 -18
View File
@@ -13,7 +13,7 @@
#include "matrix2d.h" #include "matrix2d.h"
#include "terrain.h" #include "terrain.h"
#include "terrainyieldmanager.h" #include "terrainyieldmanager.h"
#include "roadconfig.h" #include "actionneededmanager.h"
enum class ActionMode { enum class ActionMode {
None, None,
@@ -24,23 +24,6 @@ enum class ActionMode {
BuildRoad 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 { struct TileImprovement {
QString improvementId; // "farm" QString improvementId; // "farm"
int tileX; int tileX;
@@ -70,6 +53,8 @@ public:
const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; } const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; }
RoadConfigManager& roadConfigs() { return m_roadConfigManager; } RoadConfigManager& roadConfigs() { return m_roadConfigManager; }
const RoadConfigManager& roadConfigs() const { return m_roadConfigManager; } const RoadConfigManager& roadConfigs() const { return m_roadConfigManager; }
ActionNeededManager& actionNeededManager() { return m_actionNeededManager; }
const ActionNeededManager& actionNeededManager() const { return m_actionNeededManager; }
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; }
@@ -108,8 +93,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 queueCityProject(int cityId, CityProjectType type, const QString &id);
void setCityProject(int cityId, CityProjectType type, const QString &id); void setCityProject(int cityId, CityProjectType type, const QString &id);
void cancelCityProject(int cityId); void cancelCityProject(int cityId);
void removeQueuedCityProject(int cityId, int queueIndex);
void completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoints, int numPoints05); 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);
@@ -117,6 +104,8 @@ public:
void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05); void commandBuildCitySelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05); void commandBuildFarmSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandBuildRoadSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05); void commandBuildRoadSelected(Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void commandBuildRoadToTileSelected(int targetX, int targetY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void advanceRoadRoute(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05); void completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
void processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05); void processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
@@ -159,6 +148,7 @@ private:
UnitConfigManager m_unitConfigManager; UnitConfigManager m_unitConfigManager;
BuildingConfigManager m_buildingConfigManager; BuildingConfigManager m_buildingConfigManager;
RoadConfigManager m_roadConfigManager; RoadConfigManager m_roadConfigManager;
ActionNeededManager m_actionNeededManager;
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;
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

+694 -253
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -56,7 +56,6 @@ protected:
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix); void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
// City & Territory Overlays // City & Territory Overlays
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix); void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
void drawRoadsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix); void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix); void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY); bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
@@ -143,6 +142,12 @@ protected:
}; };
std::vector<CityProjectHit> m_cityProjectHits; std::vector<CityProjectHit> m_cityProjectHits;
struct CityQueueItemHit {
QRect rect;
int queueIndex;
};
std::vector<CityQueueItemHit> m_cityQueueItemHits;
QRect m_cancelProjectBtnRect; QRect m_cancelProjectBtnRect;
int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units
QRect m_tabBuildingsRect; QRect m_tabBuildingsRect;
+4
View File
@@ -17,6 +17,7 @@ RoadConfigManager::RoadConfigManager()
dirt.colorStr = "#c49a62"; dirt.colorStr = "#c49a62";
dirt.borderColorStr = "#6b4b23"; dirt.borderColorStr = "#6b4b23";
dirt.width = 3.5f; dirt.width = 3.5f;
dirt.worldWidth = 0.11f;
dirt.cost = 5; dirt.cost = 5;
m_types["dirt"] = dirt; m_types["dirt"] = dirt;
@@ -27,6 +28,7 @@ RoadConfigManager::RoadConfigManager()
stone.colorStr = "#9aa1a8"; stone.colorStr = "#9aa1a8";
stone.borderColorStr = "#464b50"; stone.borderColorStr = "#464b50";
stone.width = 4.0f; stone.width = 4.0f;
stone.worldWidth = 0.13f;
stone.cost = 15; stone.cost = 15;
m_types["stone"] = stone; m_types["stone"] = stone;
@@ -37,6 +39,7 @@ RoadConfigManager::RoadConfigManager()
rail.colorStr = "#3a3c40"; rail.colorStr = "#3a3c40";
rail.borderColorStr = "#181a1c"; rail.borderColorStr = "#181a1c";
rail.width = 4.5f; rail.width = 4.5f;
rail.worldWidth = 0.15f;
rail.cost = 30; rail.cost = 30;
m_types["rail"] = rail; m_types["rail"] = rail;
} }
@@ -72,6 +75,7 @@ bool RoadConfigManager::loadConfigFile(const QString &filePath)
rCfg.colorStr = it.value().hasChild("color") ? it.value()["color"].asString("#c49a62") : "#c49a62"; 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.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.width = it.value().hasChild("width") ? it.value()["width"].asFloat(3.5f) : 3.5f;
rCfg.worldWidth = it.value().hasChild("world_width") ? it.value()["world_width"].asFloat(0.11f) : 0.11f;
rCfg.cost = it.value().hasChild("cost") ? it.value()["cost"].asInt(5) : 5; rCfg.cost = it.value().hasChild("cost") ? it.value()["cost"].asInt(5) : 5;
m_types[rCfg.id] = rCfg; m_types[rCfg.id] = rCfg;
} }
+19
View File
@@ -4,6 +4,24 @@
#include <QString> #include <QString>
#include <QMap> #include <QMap>
#include <QColor> #include <QColor>
#include <QVector3D>
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 RoadTypeConfig { struct RoadTypeConfig {
QString id = "dirt"; QString id = "dirt";
@@ -12,6 +30,7 @@ struct RoadTypeConfig {
QString colorStr = "#c49a62"; QString colorStr = "#c49a62";
QString borderColorStr = "#6b4b23"; QString borderColorStr = "#6b4b23";
float width = 3.5f; float width = 3.5f;
float worldWidth = 0.11f;
int cost = 5; int cost = 5;
QColor getColor() const { return QColor(colorStr); } QColor getColor() const { return QColor(colorStr); }
+1
View File
@@ -35,6 +35,7 @@ bool TerrainYieldManager::loadConfigs(const QString &baseDir)
m_cityConfig.baseResourceYieldPerTick = cityRoot["base_resource_yield_per_tick"].asInt(500); m_cityConfig.baseResourceYieldPerTick = cityRoot["base_resource_yield_per_tick"].asInt(500);
m_cityConfig.tileCultureCost = cityRoot["tile_culture_cost"].asInt(10000); m_cityConfig.tileCultureCost = cityRoot["tile_culture_cost"].asInt(10000);
m_cityConfig.foodToInhabitantsRatio = cityRoot["food_to_inhabitants_ratio"].asFloat(1.0f); m_cityConfig.foodToInhabitantsRatio = cityRoot["food_to_inhabitants_ratio"].asFloat(1.0f);
m_cityConfig.maxProductionQueueSize = cityRoot["max_production_queue_size"].asInt(6);
if (cityRoot.hasChild("base_yields")) { if (cityRoot.hasChild("base_yields")) {
const YamlNode &bNode = cityRoot["base_yields"]; const YamlNode &bNode = cityRoot["base_yields"];
+2
View File
@@ -34,6 +34,7 @@ struct CityConfig {
int baseResourceYieldPerTick; int baseResourceYieldPerTick;
int tileCultureCost; int tileCultureCost;
float foodToInhabitantsRatio; float foodToInhabitantsRatio;
int maxProductionQueueSize;
int slotLongPressDurationMs; int slotLongPressDurationMs;
QStringList resourcePriority; QStringList resourcePriority;
@@ -47,6 +48,7 @@ struct CityConfig {
cityViewZoom(4.8f), cityViewZoom(4.8f),
baseResourceYieldPerTick(500), tileCultureCost(10000), baseResourceYieldPerTick(500), tileCultureCost(10000),
foodToInhabitantsRatio(1.0f), foodToInhabitantsRatio(1.0f),
maxProductionQueueSize(6),
slotLongPressDurationMs(400), slotLongPressDurationMs(400),
resourcePriority({"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"}) resourcePriority({"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"})
{ {
+4 -2
View File
@@ -32,7 +32,8 @@ SOURCES += main.cpp \
unitrenderer.cpp \ unitrenderer.cpp \
localization.cpp \ localization.cpp \
terrainmovementmanager.cpp \ terrainmovementmanager.cpp \
roadconfig.cpp roadconfig.cpp \
actionneededmanager.cpp
HEADERS += mainwindow.h \ HEADERS += mainwindow.h \
qscenerenderer.h \ qscenerenderer.h \
@@ -54,7 +55,8 @@ HEADERS += mainwindow.h \
unitrenderer.h \ unitrenderer.h \
localization.h \ localization.h \
terrainmovementmanager.h \ terrainmovementmanager.h \
roadconfig.h roadconfig.h \
actionneededmanager.h
FORMS += mainwindow.ui FORMS += mainwindow.ui
+1
View File
@@ -123,6 +123,7 @@ void Unit::stopMovement()
moveProgress = 0.0f; moveProgress = 0.0f;
state = UnitState::Idle; state = UnitState::Idle;
clearActionQueue(); clearActionQueue();
clearRoadRoute();
} }
void Unit::startBuilding(const QString &impId, float duration) void Unit::startBuilding(const QString &impId, float duration)
+19
View File
@@ -69,6 +69,25 @@ public:
std::deque<UnitAction> actionQueue; std::deque<UnitAction> actionQueue;
int maxQueueSize = 5; int maxQueueSize = 5;
// Road Construction Route (tile-by-tile road paving)
std::vector<std::pair<int, int>> roadRoute;
size_t roadRouteIndex = 0;
bool hasRoadRoute() const { return !roadRoute.empty() && roadRouteIndex < roadRoute.size(); }
void startRoadRoute(const std::vector<std::pair<int, int>> &route) {
roadRoute = route;
roadRouteIndex = 0;
}
void clearRoadRoute() {
roadRoute.clear();
roadRouteIndex = 0;
}
void advanceRoadRouteStep() {
if (!roadRoute.empty()) {
roadRouteIndex++;
}
}
bool queueAction(const UnitAction &action); bool queueAction(const UnitAction &action);
void clearActionQueue(); void clearActionQueue();
bool hasQueuedAction() const { return !actionQueue.empty(); } bool hasQueuedAction() const { return !actionQueue.empty(); }
+225 -1
View File
@@ -9,13 +9,17 @@ struct VertexPN {
}; };
UnitRenderer::UnitRenderer() UnitRenderer::UnitRenderer()
: m_program(nullptr), m_vertexCount(0), m_initialized(false) : m_program(nullptr), m_vertexCount(0), m_initialized(false), m_roadBuffersInitialized(false)
{ {
} }
UnitRenderer::~UnitRenderer() UnitRenderer::~UnitRenderer()
{ {
if (m_program) delete m_program; if (m_program) delete m_program;
if (m_roadBuffersInitialized) {
m_roadVao.destroy();
m_roadVbo.destroy();
}
} }
void UnitRenderer::initialize() void UnitRenderer::initialize()
@@ -29,6 +33,12 @@ void UnitRenderer::initialize()
m_program->link(); m_program->link();
buildCylinderMesh(); buildCylinderMesh();
m_roadVao.create();
m_roadVbo.create();
m_roadVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw);
m_roadBuffersInitialized = true;
m_initialized = true; m_initialized = true;
} }
@@ -86,6 +96,220 @@ void UnitRenderer::buildCylinderMesh()
m_vbo.release(); m_vbo.release();
} }
void UnitRenderer::renderRoads(
const std::map<std::pair<int, int>, TileRoad> &roads,
const RoadConfigManager &roadConfigs,
Matrix2D<TerrainPoint> *mapPoints,
int numPoints05,
const QMatrix4x4 &projection,
const QMatrix4x4 &vMatrix,
const QVector3D &lightDir,
int cityViewRadius,
int cityViewTileX,
int cityViewTileY)
{
if (!m_initialized || !m_program || roads.empty()) return;
if (!m_roadBuffersInitialized) {
m_roadVao.create();
m_roadVbo.create();
m_roadVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw);
m_roadBuffersInitialized = true;
}
float meanderAmp = roadConfigs.meanderAmplitude();
int subdivisions = std::max(2, roadConfigs.meanderSubdivisions());
struct RoadBatchGeometry {
std::vector<VertexPN> casingVertices;
std::vector<VertexPN> surfaceVertices;
};
std::unordered_map<std::string, RoadBatchGeometry> batches;
for (const auto &pair : roads) {
const TileRoad &road = pair.second;
if (cityViewRadius >= 0) {
if (Pathfinder::hexDistance(cityViewTileX, cityViewTileY, road.tileX, road.tileY) > cityViewRadius) {
continue;
}
}
const RoadTypeConfig *rCfg = roadConfigs.getRoadType(road.roadTypeId);
float roadW = rCfg ? rCfg->worldWidth : 0.11f;
float casingW = roadW + 0.035f;
QVector2D center = Pathfinder::hexToWorld(road.tileX, road.tileY);
RoadBatchGeometry &batch = batches[road.roadTypeId.toStdString()];
// If isolated tile with no connections, draw small circular depot / disk
if (road.connectionMask == 0) {
float gz = Unit::getTerrainHeight(center.x(), center.y(), mapPoints, numPoints05);
const int capSectors = 12;
const float pi = 3.14159265359f;
float rSurf = roadW * 0.9f;
float rCase = casingW * 0.9f;
for (int s = 0; s < capSectors; ++s) {
float a1 = 2.0f * pi * s / capSectors;
float a2 = 2.0f * pi * (s + 1) / capSectors;
batch.casingVertices.push_back({center.x(), center.y(), gz + 0.012f, 0, 0, 1});
batch.casingVertices.push_back({center.x() + std::cos(a1) * rCase, center.y() + std::sin(a1) * rCase, gz + 0.012f, 0, 0, 1});
batch.casingVertices.push_back({center.x() + std::cos(a2) * rCase, center.y() + std::sin(a2) * rCase, gz + 0.012f, 0, 0, 1});
}
for (int s = 0; s < capSectors; ++s) {
float a1 = 2.0f * pi * s / capSectors;
float a2 = 2.0f * pi * (s + 1) / capSectors;
batch.surfaceVertices.push_back({center.x(), center.y(), gz + 0.016f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x() + std::cos(a1) * rSurf, center.y() + std::sin(a1) * rSurf, gz + 0.016f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x() + std::cos(a2) * rSurf, center.y() + std::sin(a2) * rSurf, gz + 0.016f, 0, 0, 1});
}
continue;
}
// Center junction cap (circular disk covering junction gaps)
{
float gz = Unit::getTerrainHeight(center.x(), center.y(), mapPoints, numPoints05);
const int capSectors = 12;
const float pi = 3.14159265359f;
float rSurf = (roadW * 0.5f) * 1.15f;
float rCase = (casingW * 0.5f) * 1.15f;
for (int s = 0; s < capSectors; ++s) {
float a1 = 2.0f * pi * s / capSectors;
float a2 = 2.0f * pi * (s + 1) / capSectors;
batch.casingVertices.push_back({center.x(), center.y(), gz + 0.012f, 0, 0, 1});
batch.casingVertices.push_back({center.x() + std::cos(a1) * rCase, center.y() + std::sin(a1) * rCase, gz + 0.012f, 0, 0, 1});
batch.casingVertices.push_back({center.x() + std::cos(a2) * rCase, center.y() + std::sin(a2) * rCase, gz + 0.012f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x(), center.y(), gz + 0.016f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x() + std::cos(a1) * rSurf, center.y() + std::sin(a1) * rSurf, gz + 0.016f, 0, 0, 1});
batch.surfaceVertices.push_back({center.x() + std::cos(a2) * rSurf, center.y() + std::sin(a2) * rSurf, gz + 0.016f, 0, 0, 1});
}
}
// For each connected edge, generate meandering ribbon quad strip
for (int e = 0; e < 6; ++e) {
if (!road.hasConnection(e)) continue;
QVector2D endP = center + getHexEdgeMidpoint(e);
QVector2D dir = endP - center;
float len = dir.length();
QVector2D perp = (len > 0.0001f) ? QVector2D(-dir.y() / len, dir.x() / len) : QVector2D(0, 0);
float seed = std::sin(float(road.tileX * 41 + road.tileY * 113 + e * 29)) * 43758.5453f;
float meanderSign = (seed - std::floor(seed) > 0.5f) ? 1.0f : -1.0f;
const float pi = 3.14159265359f;
std::vector<QVector2D> spinePoints;
spinePoints.reserve(subdivisions + 1);
for (int i = 0; i <= subdivisions; ++i) {
float t = (float)i / (float)subdivisions;
float s = meanderSign * meanderAmp * std::sin(t * pi);
spinePoints.push_back(center + dir * t + perp * s);
}
float halfSurf = roadW * 0.5f;
float halfCase = casingW * 0.5f;
for (int i = 0; i < subdivisions; ++i) {
QVector2D p0 = spinePoints[i];
QVector2D p1 = spinePoints[i + 1];
QVector2D segDir = p1 - p0;
float segLen = segDir.length();
QVector2D segPerp = (segLen > 0.0001f) ? QVector2D(-segDir.y() / segLen, segDir.x() / segLen) : perp;
float z0 = Unit::getTerrainHeight(p0.x(), p0.y(), mapPoints, numPoints05);
float z1 = Unit::getTerrainHeight(p1.x(), p1.y(), mapPoints, numPoints05);
float cz0 = z0 + 0.012f;
float cz1 = z1 + 0.012f;
QVector3D cL0(p0.x() - segPerp.x() * halfCase, p0.y() - segPerp.y() * halfCase, cz0);
QVector3D cR0(p0.x() + segPerp.x() * halfCase, p0.y() + segPerp.y() * halfCase, cz0);
QVector3D cL1(p1.x() - segPerp.x() * halfCase, p1.y() - segPerp.y() * halfCase, cz1);
QVector3D cR1(p1.x() + segPerp.x() * halfCase, p1.y() + segPerp.y() * halfCase, cz1);
batch.casingVertices.push_back({cL0.x(), cL0.y(), cL0.z(), 0, 0, 1});
batch.casingVertices.push_back({cR0.x(), cR0.y(), cR0.z(), 0, 0, 1});
batch.casingVertices.push_back({cR1.x(), cR1.y(), cR1.z(), 0, 0, 1});
batch.casingVertices.push_back({cL0.x(), cL0.y(), cL0.z(), 0, 0, 1});
batch.casingVertices.push_back({cR1.x(), cR1.y(), cR1.z(), 0, 0, 1});
batch.casingVertices.push_back({cL1.x(), cL1.y(), cL1.z(), 0, 0, 1});
float sz0 = z0 + 0.016f;
float sz1 = z1 + 0.016f;
QVector3D sL0(p0.x() - segPerp.x() * halfSurf, p0.y() - segPerp.y() * halfSurf, sz0);
QVector3D sR0(p0.x() + segPerp.x() * halfSurf, p0.y() + segPerp.y() * halfSurf, sz0);
QVector3D sL1(p1.x() - segPerp.x() * halfSurf, p1.y() - segPerp.y() * halfSurf, sz1);
QVector3D sR1(p1.x() + segPerp.x() * halfSurf, p1.y() + segPerp.y() * halfSurf, sz1);
batch.surfaceVertices.push_back({sL0.x(), sL0.y(), sL0.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sR0.x(), sR0.y(), sR0.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sR1.x(), sR1.y(), sR1.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sL0.x(), sL0.y(), sL0.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sR1.x(), sR1.y(), sR1.z(), 0, 0, 1});
batch.surfaceVertices.push_back({sL1.x(), sL1.y(), sL1.z(), 0, 0, 1});
}
}
}
if (batches.empty()) return;
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(-2.0f, -2.0f);
m_program->bind();
m_program->setUniformValue("u_lightDir", lightDir);
QMatrix4x4 model;
m_program->setUniformValue("modelMatrix", model);
QMatrix4x4 mvp = projection * vMatrix;
m_program->setUniformValue("MVP", mvp);
m_roadVao.bind();
m_roadVbo.bind();
for (auto &bPair : batches) {
const RoadTypeConfig *rCfg = roadConfigs.getRoadType(QString::fromStdString(bPair.first));
QColor caseCol = rCfg ? rCfg->getBorderColor() : QColor(107, 75, 35);
QColor surfCol = rCfg ? rCfg->getColor() : QColor(196, 154, 98);
RoadBatchGeometry &geom = bPair.second;
// Draw Casing
if (!geom.casingVertices.empty()) {
m_roadVbo.allocate(geom.casingVertices.data(), (int)(geom.casingVertices.size() * sizeof(VertexPN)));
m_program->enableAttributeArray(0);
m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(VertexPN));
m_program->enableAttributeArray(1);
m_program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, sizeof(VertexPN));
m_program->setUniformValue("u_color", QVector4D(caseCol.redF(), caseCol.greenF(), caseCol.blueF(), 1.0f));
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)geom.casingVertices.size());
}
// Draw Surface
if (!geom.surfaceVertices.empty()) {
m_roadVbo.allocate(geom.surfaceVertices.data(), (int)(geom.surfaceVertices.size() * sizeof(VertexPN)));
m_program->enableAttributeArray(0);
m_program->setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(VertexPN));
m_program->enableAttributeArray(1);
m_program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, sizeof(VertexPN));
m_program->setUniformValue("u_color", QVector4D(surfCol.redF(), surfCol.greenF(), surfCol.blueF(), 1.0f));
glDrawArrays(GL_TRIANGLES, 0, (GLsizei)geom.surfaceVertices.size());
}
}
m_roadVbo.release();
m_roadVao.release();
m_program->release();
glDisable(GL_POLYGON_OFFSET_FILL);
}
void UnitRenderer::renderUnits( void UnitRenderer::renderUnits(
const std::vector<std::shared_ptr<Unit>> &units, const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities, const std::vector<City> &cities,
+20
View File
@@ -10,9 +10,13 @@
#include <QPointF> #include <QPointF>
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <map>
#include "unit.h" #include "unit.h"
#include "player.h" #include "player.h"
#include "city.h" #include "city.h"
#include "roadconfig.h"
#include "matrix2d.h"
#include "terrain.h"
class UnitRenderer : protected QOpenGLFunctions_4_0_Core { class UnitRenderer : protected QOpenGLFunctions_4_0_Core {
public: public:
@@ -20,6 +24,18 @@ public:
~UnitRenderer(); ~UnitRenderer();
void initialize(); void initialize();
void renderRoads(
const std::map<std::pair<int, int>, TileRoad> &roads,
const RoadConfigManager &roadConfigs,
Matrix2D<TerrainPoint> *mapPoints,
int numPoints05,
const QMatrix4x4 &projection,
const QMatrix4x4 &vMatrix,
const QVector3D &lightDir,
int cityViewRadius = -1,
int cityViewTileX = -1,
int cityViewTileY = -1
);
void renderUnits( void renderUnits(
const std::vector<std::shared_ptr<Unit>> &units, const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities, const std::vector<City> &cities,
@@ -49,6 +65,10 @@ private:
QOpenGLVertexArrayObject m_vao; QOpenGLVertexArrayObject m_vao;
int m_vertexCount; int m_vertexCount;
bool m_initialized; bool m_initialized;
QOpenGLBuffer m_roadVbo;
QOpenGLVertexArrayObject m_roadVao;
bool m_roadBuffersInitialized;
}; };
#endif // UNITRENDERER_H #endif // UNITRENDERER_H