Compare commits
6
Commits
| 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.
|
||||
> - 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]
|
||||
> **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.
|
||||
> - **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 "..."`).
|
||||
|
||||
---
|
||||
|
||||
@@ -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)
|
||||
|
||||
CityProject currentProject;
|
||||
std::vector<CityProject> projectQueue;
|
||||
std::vector<QString> completedBuildings; // IDs of completed buildings in the city
|
||||
|
||||
std::vector<std::pair<int, int>> territory; // Owned hex coordinates
|
||||
@@ -77,6 +78,34 @@ public:
|
||||
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 {
|
||||
return currentProject.isValid();
|
||||
}
|
||||
@@ -90,8 +119,53 @@ public:
|
||||
currentProject.district = district.isEmpty() ? "center" : district;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void cancelProject() {
|
||||
currentProject = CityProject();
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
default_unit_labor_cost: 1000
|
||||
unit_labor_cost_multiplier: 20
|
||||
max_production_queue_size: 6
|
||||
|
||||
# Base yield breakdown per resource (default 500 per tick)
|
||||
base_yields:
|
||||
|
||||
@@ -12,6 +12,7 @@ types:
|
||||
color: "#c49a62"
|
||||
border_color: "#6b4b23"
|
||||
width: 3.5
|
||||
world_width: 0.11
|
||||
cost: 5
|
||||
stone:
|
||||
id: "stone"
|
||||
@@ -20,6 +21,7 @@ types:
|
||||
color: "#9aa1a8"
|
||||
border_color: "#464b50"
|
||||
width: 4.0
|
||||
world_width: 0.13
|
||||
cost: 15
|
||||
rail:
|
||||
id: "rail"
|
||||
@@ -28,4 +30,5 @@ types:
|
||||
color: "#3a3c40"
|
||||
border_color: "#181a1c"
|
||||
width: 4.5
|
||||
world_width: 0.15
|
||||
cost: 30
|
||||
|
||||
+30
-5
@@ -102,7 +102,7 @@ ui:
|
||||
tip_build_farm: "Build Farm (F)"
|
||||
tip_farm_building: "Busy building... %1%"
|
||||
tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)"
|
||||
tip_build_road: "Build Road (R)"
|
||||
tip_build_road: "Build Road (R) - Click target tile to build route"
|
||||
tip_road_building: "Busy building road... %1%"
|
||||
tip_road_queued: "Build Road (Queued on arrival - click to cancel)"
|
||||
road_started: "Constructing %1... (%2 ticks)"
|
||||
@@ -110,6 +110,11 @@ ui:
|
||||
road_queued: "Build Road queued on arrival"
|
||||
road_already_here: "Tile already has a road!"
|
||||
cannot_build_road_water: "Roads cannot be built on water!"
|
||||
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:
|
||||
farm: "Farm"
|
||||
@@ -126,25 +131,35 @@ ui:
|
||||
national_info: "National: Culture: %L1 | Science: %L2 | Gold: %L3 | Faith: %L4"
|
||||
help_text: "Click a slot to assign/free Pop | Hold a slot to choose resource | Buy tile with Culture."
|
||||
current_production: "CURRENT PRODUCTION"
|
||||
production_panel_title: "CITY PRODUCTION"
|
||||
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)"
|
||||
cancel_btn: "Cancel"
|
||||
queue_header: "QUEUE (%1 / %2)"
|
||||
queue_empty: "Queue is empty"
|
||||
districts_header: "DISTRICT: CITY CENTER"
|
||||
no_buildings: "No extensions built yet"
|
||||
choose_project: "CHOOSE CITY PROJECT"
|
||||
tab_buildings: "Buildings & Extensions"
|
||||
choose_project: "AVAILABLE PROJECTS"
|
||||
tab_buildings: "Buildings"
|
||||
tab_units: "Units"
|
||||
btn_build: "Build"
|
||||
btn_train: "Train"
|
||||
btn_queue: "+ Queue"
|
||||
status_built: "BUILT"
|
||||
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)"
|
||||
unit_trained: "%1: %2 trained!"
|
||||
building_completed: "%1: %2 completed in %3!"
|
||||
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_queued: "%1 is already in the production queue!"
|
||||
growth_float: "+%1 Inhabitants | +%2 Culture"
|
||||
not_enough_culture: "Not enough Culture! (%1 needed)"
|
||||
worker_freed: "Pop worker freed"
|
||||
@@ -165,3 +180,13 @@ ui:
|
||||
kennis: "Science"
|
||||
cultuur: "Culture"
|
||||
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"
|
||||
|
||||
|
||||
+188
-18
@@ -58,6 +58,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
|
||||
QString roadsConfigPath = baseDir + "/config/roads.yaml";
|
||||
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();
|
||||
|
||||
// Initialize player national resources from configuration
|
||||
@@ -133,8 +137,12 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
|
||||
if (u->isBuilding() && u->buildProgress >= 1.0f) {
|
||||
completeUnitBuilding(*u, mapPoints, numPoints05);
|
||||
}
|
||||
if (!u->isDead() && u->state == UnitState::Idle && u->hasQueuedAction()) {
|
||||
processUnitActionQueue(*u, mapPoints, numPoints05);
|
||||
if (!u->isDead() && u->state == UnitState::Idle) {
|
||||
if (u->hasRoadRoute()) {
|
||||
advanceRoadRoute(*u, mapPoints, numPoints05);
|
||||
} else if (u->hasQueuedAction()) {
|
||||
processUnitActionQueue(*u, mapPoints, numPoints05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +170,9 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Action Needed Queue for active human player 1
|
||||
m_actionNeededManager.update(deltaTime, 1, m_units, m_cities);
|
||||
}
|
||||
|
||||
Unit* GameManager::getSelectedUnit()
|
||||
@@ -255,7 +266,7 @@ QMap<QString, int> GameManager::getTileYields(int tx, int ty, Matrix2D<TerrainPo
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
QStringList resources = cCfg.resourcePriority;
|
||||
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);
|
||||
@@ -293,7 +304,7 @@ QString GameManager::getBestTileResource(int tx, int ty, Matrix2D<TerrainPoint>
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
QStringList priority = cCfg.resourcePriority;
|
||||
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);
|
||||
@@ -455,41 +466,106 @@ bool GameManager::buyTileForCity(int cityId, int tx, int ty)
|
||||
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);
|
||||
if (!city) return;
|
||||
|
||||
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
|
||||
int maxQueue = cCfg.maxProductionQueueSize;
|
||||
|
||||
if (type == CityProjectType::Unit) {
|
||||
const UnitConfig *uCfg = m_unitConfigManager.getConfig(id);
|
||||
if (!uCfg) return;
|
||||
city->startProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost);
|
||||
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
|
||||
.arg(city->name).arg(uCfg->name);
|
||||
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
|
||||
|
||||
if (!city->hasActiveProject()) {
|
||||
city->startProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost);
|
||||
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
|
||||
.arg(city->name).arg(uCfg->name);
|
||||
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
|
||||
} else {
|
||||
if ((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) {
|
||||
const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(id);
|
||||
if (!bCfg) return;
|
||||
|
||||
if (city->hasBuilding(id)) {
|
||||
QString msg = Localization::instance().get("ui.city.already_built", "%1 already built!").arg(bCfg->name);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
city->startProject(CityProjectType::Building, id, bCfg->name, bCfg->laborCost, bCfg->district);
|
||||
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
|
||||
.arg(city->name).arg(bCfg->name);
|
||||
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
|
||||
|
||||
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);
|
||||
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2")
|
||||
.arg(city->name).arg(bCfg->name);
|
||||
addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 230, 80), 2.5f);
|
||||
} 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)
|
||||
{
|
||||
City *city = getCityById(cityId);
|
||||
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);
|
||||
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)
|
||||
@@ -540,8 +616,16 @@ void GameManager::completeCityProject(City &city, Matrix2D<TerrainPoint> *mapPoi
|
||||
}
|
||||
|
||||
int excess = city.currentProject.currentLabor - city.currentProject.laborCost;
|
||||
city.accumulatedLabor += std::max(0, excess);
|
||||
city.cancelProject();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
selected->clearActionQueue();
|
||||
selected->clearRoadRoute();
|
||||
selected->setPath(path);
|
||||
addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"),
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
int tx = unit.tileX;
|
||||
@@ -981,6 +1146,11 @@ void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *mapPo
|
||||
QString roadName = rCfg ? rCfg->name : "Road";
|
||||
addFloatingText(Localization::instance().get("ui.unit.road_completed", "%1 completed!").arg(roadName),
|
||||
QVector3D(w.x(), w.y(), gz + 1.2f), QColor(240, 210, 100), 2.8f);
|
||||
|
||||
if (unit.hasRoadRoute()) {
|
||||
unit.advanceRoadRouteStep();
|
||||
advanceRoadRoute(unit, mapPoints, numPoints05);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+8
-18
@@ -13,7 +13,7 @@
|
||||
#include "matrix2d.h"
|
||||
#include "terrain.h"
|
||||
#include "terrainyieldmanager.h"
|
||||
#include "roadconfig.h"
|
||||
#include "actionneededmanager.h"
|
||||
|
||||
enum class ActionMode {
|
||||
None,
|
||||
@@ -24,23 +24,6 @@ enum class ActionMode {
|
||||
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 {
|
||||
QString improvementId; // "farm"
|
||||
int tileX;
|
||||
@@ -70,6 +53,8 @@ public:
|
||||
const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; }
|
||||
RoadConfigManager& roadConfigs() { 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<City>& getCities() const { return m_cities; }
|
||||
@@ -108,8 +93,10 @@ public:
|
||||
bool isTileOwned(int tx, int ty, int *outOwnerCityId = nullptr) const;
|
||||
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 cancelCityProject(int cityId);
|
||||
void removeQueuedCityProject(int cityId, int queueIndex);
|
||||
void completeCityProject(City &city, 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 commandBuildFarmSelected(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 processUnitActionQueue(Unit &unit, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
|
||||
@@ -159,6 +148,7 @@ private:
|
||||
UnitConfigManager m_unitConfigManager;
|
||||
BuildingConfigManager m_buildingConfigManager;
|
||||
RoadConfigManager m_roadConfigManager;
|
||||
ActionNeededManager m_actionNeededManager;
|
||||
std::vector<std::shared_ptr<Unit>> m_units;
|
||||
std::vector<City> m_cities;
|
||||
std::vector<FloatingText> m_floatingTexts;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
+705
-264
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -56,7 +56,6 @@ protected:
|
||||
void drawFloatingTexts(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
// City & Territory Overlays
|
||||
void drawCityBorders(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void drawRoadsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void drawImprovementsOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
void drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vMatrix);
|
||||
bool screenToHex(const QPoint &screenPos, const QMatrix4x4 &vMatrix, int &outX, int &outY);
|
||||
@@ -143,6 +142,12 @@ protected:
|
||||
};
|
||||
std::vector<CityProjectHit> m_cityProjectHits;
|
||||
|
||||
struct CityQueueItemHit {
|
||||
QRect rect;
|
||||
int queueIndex;
|
||||
};
|
||||
std::vector<CityQueueItemHit> m_cityQueueItemHits;
|
||||
|
||||
QRect m_cancelProjectBtnRect;
|
||||
int m_cityProjectTab = 0; // 0 = Buildings, 1 = Units
|
||||
QRect m_tabBuildingsRect;
|
||||
|
||||
@@ -17,6 +17,7 @@ RoadConfigManager::RoadConfigManager()
|
||||
dirt.colorStr = "#c49a62";
|
||||
dirt.borderColorStr = "#6b4b23";
|
||||
dirt.width = 3.5f;
|
||||
dirt.worldWidth = 0.11f;
|
||||
dirt.cost = 5;
|
||||
m_types["dirt"] = dirt;
|
||||
|
||||
@@ -27,6 +28,7 @@ RoadConfigManager::RoadConfigManager()
|
||||
stone.colorStr = "#9aa1a8";
|
||||
stone.borderColorStr = "#464b50";
|
||||
stone.width = 4.0f;
|
||||
stone.worldWidth = 0.13f;
|
||||
stone.cost = 15;
|
||||
m_types["stone"] = stone;
|
||||
|
||||
@@ -37,6 +39,7 @@ RoadConfigManager::RoadConfigManager()
|
||||
rail.colorStr = "#3a3c40";
|
||||
rail.borderColorStr = "#181a1c";
|
||||
rail.width = 4.5f;
|
||||
rail.worldWidth = 0.15f;
|
||||
rail.cost = 30;
|
||||
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.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.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;
|
||||
m_types[rCfg.id] = rCfg;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,24 @@
|
||||
#include <QString>
|
||||
#include <QMap>
|
||||
#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 {
|
||||
QString id = "dirt";
|
||||
@@ -12,6 +30,7 @@ struct RoadTypeConfig {
|
||||
QString colorStr = "#c49a62";
|
||||
QString borderColorStr = "#6b4b23";
|
||||
float width = 3.5f;
|
||||
float worldWidth = 0.11f;
|
||||
int cost = 5;
|
||||
|
||||
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.tileCultureCost = cityRoot["tile_culture_cost"].asInt(10000);
|
||||
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")) {
|
||||
const YamlNode &bNode = cityRoot["base_yields"];
|
||||
|
||||
@@ -34,6 +34,7 @@ struct CityConfig {
|
||||
int baseResourceYieldPerTick;
|
||||
int tileCultureCost;
|
||||
float foodToInhabitantsRatio;
|
||||
int maxProductionQueueSize;
|
||||
|
||||
int slotLongPressDurationMs;
|
||||
QStringList resourcePriority;
|
||||
@@ -47,6 +48,7 @@ struct CityConfig {
|
||||
cityViewZoom(4.8f),
|
||||
baseResourceYieldPerTick(500), tileCultureCost(10000),
|
||||
foodToInhabitantsRatio(1.0f),
|
||||
maxProductionQueueSize(6),
|
||||
slotLongPressDurationMs(400),
|
||||
resourcePriority({"voedsel", "arbeid", "goud", "kennis", "cultuur", "geloof"})
|
||||
{
|
||||
|
||||
+4
-2
@@ -32,7 +32,8 @@ SOURCES += main.cpp \
|
||||
unitrenderer.cpp \
|
||||
localization.cpp \
|
||||
terrainmovementmanager.cpp \
|
||||
roadconfig.cpp
|
||||
roadconfig.cpp \
|
||||
actionneededmanager.cpp
|
||||
|
||||
HEADERS += mainwindow.h \
|
||||
qscenerenderer.h \
|
||||
@@ -54,7 +55,8 @@ HEADERS += mainwindow.h \
|
||||
unitrenderer.h \
|
||||
localization.h \
|
||||
terrainmovementmanager.h \
|
||||
roadconfig.h
|
||||
roadconfig.h \
|
||||
actionneededmanager.h
|
||||
|
||||
FORMS += mainwindow.ui
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ void Unit::stopMovement()
|
||||
moveProgress = 0.0f;
|
||||
state = UnitState::Idle;
|
||||
clearActionQueue();
|
||||
clearRoadRoute();
|
||||
}
|
||||
|
||||
void Unit::startBuilding(const QString &impId, float duration)
|
||||
|
||||
@@ -69,6 +69,25 @@ public:
|
||||
std::deque<UnitAction> actionQueue;
|
||||
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);
|
||||
void clearActionQueue();
|
||||
bool hasQueuedAction() const { return !actionQueue.empty(); }
|
||||
|
||||
+225
-1
@@ -9,13 +9,17 @@ struct VertexPN {
|
||||
};
|
||||
|
||||
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()
|
||||
{
|
||||
if (m_program) delete m_program;
|
||||
if (m_roadBuffersInitialized) {
|
||||
m_roadVao.destroy();
|
||||
m_roadVbo.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void UnitRenderer::initialize()
|
||||
@@ -29,6 +33,12 @@ void UnitRenderer::initialize()
|
||||
m_program->link();
|
||||
|
||||
buildCylinderMesh();
|
||||
|
||||
m_roadVao.create();
|
||||
m_roadVbo.create();
|
||||
m_roadVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw);
|
||||
m_roadBuffersInitialized = true;
|
||||
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
@@ -86,6 +96,220 @@ void UnitRenderer::buildCylinderMesh()
|
||||
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(
|
||||
const std::vector<std::shared_ptr<Unit>> &units,
|
||||
const std::vector<City> &cities,
|
||||
|
||||
@@ -10,9 +10,13 @@
|
||||
#include <QPointF>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include "unit.h"
|
||||
#include "player.h"
|
||||
#include "city.h"
|
||||
#include "roadconfig.h"
|
||||
#include "matrix2d.h"
|
||||
#include "terrain.h"
|
||||
|
||||
class UnitRenderer : protected QOpenGLFunctions_4_0_Core {
|
||||
public:
|
||||
@@ -20,6 +24,18 @@ public:
|
||||
~UnitRenderer();
|
||||
|
||||
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(
|
||||
const std::vector<std::shared_ptr<Unit>> &units,
|
||||
const std::vector<City> &cities,
|
||||
@@ -49,6 +65,10 @@ private:
|
||||
QOpenGLVertexArrayObject m_vao;
|
||||
int m_vertexCount;
|
||||
bool m_initialized;
|
||||
|
||||
QOpenGLBuffer m_roadVbo;
|
||||
QOpenGLVertexArrayObject m_roadVao;
|
||||
bool m_roadBuffersInitialized;
|
||||
};
|
||||
|
||||
#endif // UNITRENDERER_H
|
||||
|
||||
Reference in New Issue
Block a user