Implement action-needed queue bar at bottom of screen with oldest-first ordering and dismissal
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -180,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"
|
||||
|
||||
|
||||
+98
-1
@@ -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,11 +137,15 @@ 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()) {
|
||||
if (!u->isDead() && u->state == UnitState::Idle) {
|
||||
if (u->hasRoadRoute()) {
|
||||
advanceRoadRoute(*u, mapPoints, numPoints05);
|
||||
} else if (u->hasQueuedAction()) {
|
||||
processUnitActionQueue(*u, mapPoints, numPoints05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up dead units
|
||||
for (auto it = m_units.begin(); it != m_units.end(); ) {
|
||||
@@ -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()
|
||||
@@ -663,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));
|
||||
@@ -1032,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;
|
||||
@@ -1054,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;
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -13,7 +13,7 @@
|
||||
#include "matrix2d.h"
|
||||
#include "terrain.h"
|
||||
#include "terrainyieldmanager.h"
|
||||
#include "roadconfig.h"
|
||||
#include "actionneededmanager.h"
|
||||
|
||||
enum class ActionMode {
|
||||
None,
|
||||
@@ -70,6 +70,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; }
|
||||
@@ -119,6 +121,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);
|
||||
|
||||
@@ -161,6 +165,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;
|
||||
|
||||
@@ -2267,6 +2267,29 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
|
||||
return; // In City View, ignore all other clicks
|
||||
}
|
||||
|
||||
// Action Queue items (Bottom HUD notification bar)
|
||||
for (const auto &hit : m_actionItemHits) {
|
||||
if (hit.dismissRect.contains(event->pos()) && event->button() == Qt::LeftButton) {
|
||||
GameManager::instance().actionNeededManager().dismissItem(hit.type, hit.entityId);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
if (hit.rect.contains(event->pos())) {
|
||||
if (event->button() == Qt::RightButton) {
|
||||
GameManager::instance().actionNeededManager().dismissItem(hit.type, hit.entityId);
|
||||
update();
|
||||
return;
|
||||
} else if (event->button() == Qt::LeftButton) {
|
||||
handleActionItemClick(hit.type, hit.entityId);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_actionQueueHeaderRect.contains(event->pos())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicked on a City Banner to open City View
|
||||
for (const auto &cb : m_cityBannerHits) {
|
||||
if (cb.rect.contains(event->pos())) {
|
||||
@@ -2478,6 +2501,20 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
|
||||
if (zRot >= 360.0f) zRot -= 360.0f;
|
||||
update();
|
||||
}
|
||||
|
||||
bool onActionItem = false;
|
||||
for (const auto &hit : m_actionItemHits) {
|
||||
if (hit.rect.contains(event->pos())) {
|
||||
onActionItem = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (onActionItem) {
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
} else {
|
||||
unsetCursor();
|
||||
}
|
||||
|
||||
lastPos = event->pos();
|
||||
}
|
||||
|
||||
@@ -2783,6 +2820,7 @@ void QSceneRenderer::paintGL()
|
||||
if (selected) {
|
||||
drawUnitCard(painter, selected);
|
||||
}
|
||||
drawActionNeededQueue(painter);
|
||||
} else {
|
||||
drawUnitsOverlay(painter, vMatrix);
|
||||
drawFloatingTexts(painter, vMatrix);
|
||||
@@ -2790,3 +2828,190 @@ void QSceneRenderer::paintGL()
|
||||
}
|
||||
painter.end();
|
||||
}
|
||||
|
||||
void QSceneRenderer::drawActionNeededQueue(QPainter &painter)
|
||||
{
|
||||
m_actionItemHits.clear();
|
||||
m_actionQueueHeaderRect = QRect();
|
||||
|
||||
const ActionNeededConfig &cfg = GameManager::instance().actionNeededManager().config();
|
||||
if (!cfg.enabled) return;
|
||||
|
||||
std::vector<ActionItem> visibleItems = GameManager::instance().actionNeededManager().getVisibleItems();
|
||||
if (visibleItems.empty()) return;
|
||||
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
QRect minimapCard = getMinimapCardRect();
|
||||
int minLeft = minimapCard.right() + 14;
|
||||
Unit *selected = GameManager::instance().getSelectedUnit();
|
||||
int maxRight = selected ? (m_unitCardRect.left() - 14) : (width() - 16);
|
||||
int availableW = maxRight - minLeft;
|
||||
if (availableW < 120) return;
|
||||
|
||||
int itemW = cfg.itemWidth;
|
||||
int itemH = cfg.itemHeight;
|
||||
int spacing = cfg.itemSpacing;
|
||||
int headerW = 82;
|
||||
|
||||
int remainingForItems = availableW - headerW - spacing;
|
||||
int maxItemsThatFit = std::max(1, remainingForItems / (itemW + spacing));
|
||||
int numToRender = std::min((int)visibleItems.size(), std::min(cfg.maxVisibleItems, maxItemsThatFit));
|
||||
bool hasOverflow = (int)visibleItems.size() > numToRender;
|
||||
int overflowW = hasOverflow ? 54 : 0;
|
||||
if (hasOverflow && remainingForItems < numToRender * (itemW + spacing) + overflowW) {
|
||||
numToRender = std::max(1, numToRender - 1);
|
||||
}
|
||||
|
||||
int totalW = headerW + spacing + numToRender * itemW + (numToRender - 1) * spacing + (hasOverflow ? (spacing + overflowW) : 0);
|
||||
int startX = minLeft + (availableW - totalW) / 2;
|
||||
if (startX < minLeft) startX = minLeft;
|
||||
int bottomY = height() - itemH - cfg.bottomOffset;
|
||||
|
||||
// 1. Header badge
|
||||
QRect headerRect(startX, bottomY, headerW, itemH);
|
||||
m_actionQueueHeaderRect = headerRect;
|
||||
painter.setBrush(cfg.cardBgColor);
|
||||
painter.setPen(QPen(QColor(70, 95, 130, 180), 1.2));
|
||||
painter.drawRoundedRect(headerRect, cfg.borderRadius, cfg.borderRadius);
|
||||
|
||||
float pulse = cfg.pulseAnimation ? (sin(GameManager::instance().actionNeededManager().getElapsedGameTime() * cfg.pulseSpeed) * 0.5f + 0.5f) : 1.0f;
|
||||
QRect badgeRect(headerRect.left() + 8, headerRect.top() + (itemH - 20) / 2, 20, 20);
|
||||
QColor badgeCol(255, 170, 40);
|
||||
badgeCol.setAlpha((int)(170 + 80 * pulse));
|
||||
painter.setBrush(badgeCol);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawEllipse(badgeRect);
|
||||
|
||||
painter.setFont(QFont("sans-serif", 9, QFont::Bold));
|
||||
painter.setPen(Qt::white);
|
||||
painter.drawText(badgeRect, Qt::AlignCenter, QString::number(visibleItems.size()));
|
||||
|
||||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||||
painter.setPen(QColor(215, 230, 250));
|
||||
QRect headerTextRect(badgeRect.right() + 6, headerRect.top(), headerRect.right() - badgeRect.right() - 8, itemH);
|
||||
painter.drawText(headerTextRect, Qt::AlignLeft | Qt::AlignVCenter, Localization::instance().get("ui.action_needed.header_title", "ACTIONS"));
|
||||
|
||||
// 2. Render cards (oldest first)
|
||||
for (int i = 0; i < numToRender; ++i) {
|
||||
const ActionItem &item = visibleItems[i];
|
||||
int itemX = startX + headerW + spacing + i * (itemW + spacing);
|
||||
QRect itemRect(itemX, bottomY, itemW, itemH);
|
||||
QRect dismissRect(itemRect.right() - 16, itemRect.top() + 3, 13, 13);
|
||||
m_actionItemHits.push_back({itemRect, dismissRect, item.type, item.entityId});
|
||||
|
||||
bool isHovered = itemRect.contains(m_mouseCurrentPos);
|
||||
|
||||
QString title;
|
||||
QString subtitle;
|
||||
QColor accent;
|
||||
QImage icon;
|
||||
|
||||
if (item.type == ActionNeededType::UnitIdle) {
|
||||
Unit *u = GameManager::instance().getUnitById(item.entityId);
|
||||
title = u ? u->config.name.toUpper() : Localization::instance().get("ui.unit.civilian_unit", "UNIT");
|
||||
subtitle = Localization::instance().get("ui.action_needed.unit_idle_desc", "Needs orders");
|
||||
accent = cfg.unitIdleColor;
|
||||
const Player *p = u ? GameManager::instance().players().getPlayer(u->playerId) : nullptr;
|
||||
QColor pCol = p ? p->primaryColor : Qt::blue;
|
||||
icon = u ? getTintedIcon(u->config.iconPath, pCol) : QImage();
|
||||
} else if (item.type == ActionNeededType::CityNoProduction) {
|
||||
City *c = GameManager::instance().getCityById(item.entityId);
|
||||
title = c ? c->name : Localization::instance().get("ui.city.center", "CITY");
|
||||
subtitle = Localization::instance().get("ui.action_needed.city_no_production_desc", "No production");
|
||||
accent = cfg.cityNoProdColor;
|
||||
icon = getTintedIcon("images/resources/arbeid.png", accent);
|
||||
} else if (item.type == ActionNeededType::CityFreePopulation) {
|
||||
City *c = GameManager::instance().getCityById(item.entityId);
|
||||
title = c ? c->name : Localization::instance().get("ui.city.center", "CITY");
|
||||
subtitle = Localization::instance().get("ui.action_needed.city_free_pop_desc", "+%1 Free Pop").arg(item.auxValue);
|
||||
accent = cfg.cityFreePopColor;
|
||||
icon = getTintedIcon("images/resources/citizen.png", accent);
|
||||
}
|
||||
|
||||
// Draw card background
|
||||
QColor bg = cfg.cardBgColor;
|
||||
if (isHovered) {
|
||||
bg = bg.lighter(130);
|
||||
}
|
||||
painter.setBrush(bg);
|
||||
painter.setPen(QPen(isHovered ? accent : QColor(70, 95, 130, 160), isHovered ? 1.5 : 1.0));
|
||||
painter.drawRoundedRect(itemRect, cfg.borderRadius, cfg.borderRadius);
|
||||
|
||||
// Accent stripe on the left edge
|
||||
QRect stripeRect(itemRect.left() + 2, itemRect.top() + 3, 3, itemH - 6);
|
||||
painter.fillRect(stripeRect, accent);
|
||||
|
||||
// Icon badge
|
||||
QRect iconRect(itemRect.left() + 9, itemRect.top() + (itemH - 24) / 2, 24, 24);
|
||||
QColor iconBg = accent;
|
||||
iconBg.setAlpha(40);
|
||||
painter.setBrush(iconBg);
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawEllipse(iconRect);
|
||||
if (!icon.isNull()) {
|
||||
painter.drawImage(iconRect.adjusted(3, 3, -3, -3), icon);
|
||||
}
|
||||
|
||||
// Title and subtitle text
|
||||
int textLeft = iconRect.right() + 7;
|
||||
int textW = dismissRect.left() - textLeft - 2;
|
||||
|
||||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||||
painter.setPen(QColor(240, 245, 255));
|
||||
painter.drawText(QRect(textLeft, itemRect.top() + 4, textW, 16), Qt::AlignLeft | Qt::AlignVCenter, title);
|
||||
|
||||
painter.setFont(QFont("sans-serif", 7));
|
||||
painter.setPen(accent);
|
||||
painter.drawText(QRect(textLeft, itemRect.top() + 20, textW, 15), Qt::AlignLeft | Qt::AlignVCenter, subtitle);
|
||||
|
||||
// Dismiss [x]
|
||||
bool isDismissHovered = dismissRect.contains(m_mouseCurrentPos);
|
||||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||||
painter.setPen(isDismissHovered ? QColor(255, 120, 120) : QColor(130, 155, 180, 180));
|
||||
painter.drawText(dismissRect, Qt::AlignCenter, "×");
|
||||
}
|
||||
|
||||
// 3. Overflow indicator
|
||||
if (hasOverflow) {
|
||||
int moreCount = (int)visibleItems.size() - numToRender;
|
||||
QRect ofRect(startX + headerW + spacing + numToRender * (itemW + spacing), bottomY, overflowW, itemH);
|
||||
painter.setBrush(QColor(20, 26, 38, 220));
|
||||
painter.setPen(QPen(QColor(70, 95, 130, 150), 1.0));
|
||||
painter.drawRoundedRect(ofRect, cfg.borderRadius, cfg.borderRadius);
|
||||
painter.setFont(QFont("sans-serif", 8, QFont::Bold));
|
||||
painter.setPen(QColor(180, 210, 240));
|
||||
QString ofText = Localization::instance().get("ui.action_needed.more_items", "+%1").arg(moreCount);
|
||||
painter.drawText(ofRect, Qt::AlignCenter, ofText);
|
||||
}
|
||||
}
|
||||
|
||||
void QSceneRenderer::handleActionItemClick(ActionNeededType type, int entityId)
|
||||
{
|
||||
if (type == ActionNeededType::UnitIdle) {
|
||||
if (GameManager::instance().isInCityView()) {
|
||||
GameManager::instance().exitCityView();
|
||||
}
|
||||
Unit *u = GameManager::instance().getUnitById(entityId);
|
||||
if (u) {
|
||||
GameManager::instance().selectUnit(entityId);
|
||||
cameraPosition.setX(u->worldPos.x());
|
||||
cameraPosition.setY(u->worldPos.y());
|
||||
}
|
||||
} else if (type == ActionNeededType::CityNoProduction || type == ActionNeededType::CityFreePopulation) {
|
||||
City *c = GameManager::instance().getCityById(entityId);
|
||||
if (c) {
|
||||
if (!GameManager::instance().isInCityView()) {
|
||||
m_savedCamPos = cameraPosition;
|
||||
m_savedZoom = m_zoom;
|
||||
m_savedZRot = zRot;
|
||||
}
|
||||
GameManager::instance().enterCityView(entityId);
|
||||
cameraPosition.setX(c->worldPos.x());
|
||||
cameraPosition.setY(c->worldPos.y());
|
||||
if (type == ActionNeededType::CityNoProduction) {
|
||||
m_cityProjectTab = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user