Compare commits
6
Commits
b1f67be139
...
745089df7c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
745089df7c | ||
|
|
c47673575b | ||
|
|
705aa0f0f0 | ||
|
|
980e777199 | ||
|
|
4eda595861 | ||
|
|
996030e9d3 |
@@ -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 "..."`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
+694
-253
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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"];
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user