Implement action-needed queue bar at bottom of screen with oldest-first ordering and dismissal

This commit is contained in:
bart
2026-11-08 22:39:20 +01:00
parent 705aa0f0f0
commit c47673575b
8 changed files with 689 additions and 5 deletions
+243
View File
@@ -0,0 +1,243 @@
#include "actionneededmanager.h"
#include "yamlparser.h"
#include "unit.h"
#include "city.h"
#include <algorithm>
#include <set>
#include <QDebug>
ActionNeededManager::ActionNeededManager()
: m_elapsedGameTime(0.0)
{
}
bool ActionNeededManager::loadConfigFile(const QString &filePath)
{
YamlNode root = YamlParser::parseFile(filePath);
if (root.children.isEmpty()) {
qWarning() << "Failed to parse action needed YAML, using defaults:" << filePath;
return false;
}
const YamlNode &node = root.hasChild("action_needed") ? root["action_needed"] : root;
if (node.hasChild("enabled")) {
m_config.enabled = node["enabled"].asBool(true);
}
if (node.hasChild("track_idle_units")) {
m_config.trackIdleUnits = node["track_idle_units"].asBool(true);
}
if (node.hasChild("track_city_no_production")) {
m_config.trackCityNoProduction = node["track_city_no_production"].asBool(true);
}
if (node.hasChild("track_city_free_population")) {
m_config.trackCityFreePopulation = node["track_city_free_population"].asBool(true);
}
if (node.hasChild("max_visible_items")) {
m_config.maxVisibleItems = node["max_visible_items"].asInt(8);
}
if (node.hasChild("item_width")) {
m_config.itemWidth = node["item_width"].asInt(155);
}
if (node.hasChild("item_height")) {
m_config.itemHeight = node["item_height"].asInt(42);
}
if (node.hasChild("bottom_offset")) {
m_config.bottomOffset = node["bottom_offset"].asInt(16);
}
if (node.hasChild("item_spacing")) {
m_config.itemSpacing = node["item_spacing"].asInt(8);
}
if (node.hasChild("border_radius")) {
m_config.borderRadius = node["border_radius"].asInt(6);
}
if (node.hasChild("pulse_animation")) {
m_config.pulseAnimation = node["pulse_animation"].asBool(true);
}
if (node.hasChild("pulse_speed")) {
m_config.pulseSpeed = node["pulse_speed"].asFloat(3.0f);
}
if (node.hasChild("unit_idle_color")) {
QString hex = node["unit_idle_color"].asString("#3CA0FF");
m_config.unitIdleColor = QColor(hex);
}
if (node.hasChild("city_no_prod_color")) {
QString hex = node["city_no_prod_color"].asString("#FFA028");
m_config.cityNoProdColor = QColor(hex);
}
if (node.hasChild("city_free_pop_color")) {
QString hex = node["city_free_pop_color"].asString("#46DC82");
m_config.cityFreePopColor = QColor(hex);
}
if (node.hasChild("card_bg_color")) {
QString hex = node["card_bg_color"].asString("#121822");
m_config.cardBgColor = QColor(hex);
}
if (node.hasChild("card_bg_alpha")) {
m_config.cardBgAlpha = node["card_bg_alpha"].asInt(235);
}
m_config.cardBgColor.setAlpha(m_config.cardBgAlpha);
qDebug() << "ActionNeededManager config loaded from" << filePath;
return true;
}
void ActionNeededManager::update(float deltaTime, int activePlayerId,
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities)
{
if (!m_config.enabled) return;
m_elapsedGameTime += static_cast<double>(deltaTime);
std::set<QString> currentConditionKeys;
// 1. Idle Units waiting for action
if (m_config.trackIdleUnits) {
for (const auto &u : units) {
if (!u || u->isDead() || u->playerId != activePlayerId) continue;
bool isIdle = (u->state == UnitState::Idle)
&& (u->path.empty() || u->pathIndex >= u->path.size())
&& !u->hasQueuedAction()
&& !u->hasRoadRoute()
&& !u->isBuilding();
if (isIdle) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::UnitIdle)).arg(u->id);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::UnitIdle && item.entityId == u->id;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::UnitIdle;
newItem.entityId = u->id;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.dismissed = false;
m_items.push_back(newItem);
}
}
}
}
// 2. Cities waiting for action
for (const auto &c : cities) {
if (c.playerId != activePlayerId) continue;
// a) City is not building anything
if (m_config.trackCityNoProduction) {
bool noProd = !c.hasActiveProject();
if (noProd) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::CityNoProduction)).arg(c.id);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::CityNoProduction && item.entityId == c.id;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::CityNoProduction;
newItem.entityId = c.id;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.dismissed = false;
m_items.push_back(newItem);
}
}
}
// b) City has free / unassigned population
if (m_config.trackCityFreePopulation) {
int freePop = c.getAvailablePop();
if (freePop > 0) {
QString key = QString("%1_%2").arg(static_cast<int>(ActionNeededType::CityFreePopulation)).arg(c.id);
currentConditionKeys.insert(key);
auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return item.type == ActionNeededType::CityFreePopulation && item.entityId == c.id;
});
if (it == m_items.end()) {
ActionItem newItem;
newItem.type = ActionNeededType::CityFreePopulation;
newItem.entityId = c.id;
newItem.firstDetectedTime = m_elapsedGameTime;
newItem.auxValue = freePop;
newItem.dismissed = false;
m_items.push_back(newItem);
} else {
// If free population increased since it was dismissed, reactivate it!
if (freePop > it->dismissedAuxValue) {
it->dismissed = false;
}
it->auxValue = freePop;
}
}
}
}
// 3. Problem resolved -> remove from queue!
m_items.erase(std::remove_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) {
return currentConditionKeys.find(item.getKey()) == currentConditionKeys.end();
}), m_items.end());
}
std::vector<ActionItem> ActionNeededManager::getVisibleItems() const
{
std::vector<ActionItem> visible;
visible.reserve(m_items.size());
for (const auto &item : m_items) {
if (!item.dismissed) {
visible.push_back(item);
}
}
// Sort in order of oldest first (firstDetectedTime ASC)
std::sort(visible.begin(), visible.end(), [](const ActionItem &a, const ActionItem &b) {
if (a.firstDetectedTime != b.firstDetectedTime)
return a.firstDetectedTime < b.firstDetectedTime;
return a.entityId < b.entityId;
});
return visible;
}
int ActionNeededManager::getVisibleCount() const
{
int count = 0;
for (const auto &item : m_items) {
if (!item.dismissed) {
count++;
}
}
return count;
}
void ActionNeededManager::dismissItem(ActionNeededType type, int entityId)
{
for (auto &item : m_items) {
if (item.type == type && item.entityId == entityId) {
item.dismissed = true;
item.dismissedAuxValue = item.auxValue;
break;
}
}
}
void ActionNeededManager::resetDismissals()
{
for (auto &item : m_items) {
item.dismissed = false;
item.dismissedAuxValue = 0;
}
}
void ActionNeededManager::reset()
{
m_items.clear();
m_elapsedGameTime = 0.0;
}
+79
View File
@@ -0,0 +1,79 @@
#ifndef ACTIONNEEDEDMANAGER_H
#define ACTIONNEEDEDMANAGER_H
#include <QString>
#include <QColor>
#include <vector>
#include <memory>
class Unit;
class City;
enum class ActionNeededType {
UnitIdle = 0,
CityNoProduction = 1,
CityFreePopulation = 2
};
struct ActionItem {
ActionNeededType type = ActionNeededType::UnitIdle;
int entityId = -1;
double firstDetectedTime = 0.0; // Monotonic game time in seconds
int auxValue = 0; // e.g. free population count
bool dismissed = false;
int dismissedAuxValue = 0; // Value at the time of dismissal
QString getKey() const {
return QString("%1_%2").arg(static_cast<int>(type)).arg(entityId);
}
};
struct ActionNeededConfig {
bool enabled = true;
bool trackIdleUnits = true;
bool trackCityNoProduction = true;
bool trackCityFreePopulation = true;
int maxVisibleItems = 8;
int itemWidth = 155;
int itemHeight = 42;
int bottomOffset = 16;
int itemSpacing = 8;
int borderRadius = 6;
bool pulseAnimation = true;
float pulseSpeed = 3.0f;
QColor unitIdleColor = QColor(60, 160, 255);
QColor cityNoProdColor = QColor(255, 160, 40);
QColor cityFreePopColor = QColor(70, 220, 130);
QColor cardBgColor = QColor(18, 24, 34);
int cardBgAlpha = 235;
};
class ActionNeededManager {
public:
ActionNeededManager();
bool loadConfigFile(const QString &filePath);
const ActionNeededConfig& config() const { return m_config; }
void update(float deltaTime, int activePlayerId,
const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities);
std::vector<ActionItem> getVisibleItems() const;
int getVisibleCount() const;
void dismissItem(ActionNeededType type, int entityId);
void resetDismissals();
double getElapsedGameTime() const { return m_elapsedGameTime; }
void reset();
private:
ActionNeededConfig m_config;
std::vector<ActionItem> m_items;
double m_elapsedGameTime;
};
#endif // ACTIONNEEDEDMANAGER_H
+23
View File
@@ -0,0 +1,23 @@
# Configuration for the Action Needed Queue (Bottom HUD notification bar)
action_needed:
enabled: true
track_idle_units: true
track_city_no_production: true
track_city_free_population: true
max_visible_items: 8
# Visual & Layout parameters
item_width: 155
item_height: 42
bottom_offset: 16
item_spacing: 8
border_radius: 6
pulse_animation: true
pulse_speed: 3.0
# Colors
unit_idle_color: "#3CA0FF"
city_no_prod_color: "#FFA028"
city_free_pop_color: "#46DC82"
card_bg_color: "#121822"
card_bg_alpha: 235
+10
View File
@@ -180,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"
+98 -1
View File
@@ -58,6 +58,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
QString roadsConfigPath = baseDir + "/config/roads.yaml"; QString roadsConfigPath = baseDir + "/config/roads.yaml";
m_roadConfigManager.loadConfigFile(roadsConfigPath); m_roadConfigManager.loadConfigFile(roadsConfigPath);
// Load action needed queue configuration
QString actionNeededConfigPath = baseDir + "/config/action_needed.yaml";
m_actionNeededManager.loadConfigFile(actionNeededConfigPath);
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
// Initialize player national resources from configuration // Initialize player national resources from configuration
@@ -133,11 +137,15 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
if (u->isBuilding() && u->buildProgress >= 1.0f) { if (u->isBuilding() && u->buildProgress >= 1.0f) {
completeUnitBuilding(*u, mapPoints, numPoints05); completeUnitBuilding(*u, mapPoints, numPoints05);
} }
if (!u->isDead() && u->state == UnitState::Idle && u->hasQueuedAction()) { if (!u->isDead() && u->state == UnitState::Idle) {
if (u->hasRoadRoute()) {
advanceRoadRoute(*u, mapPoints, numPoints05);
} else if (u->hasQueuedAction()) {
processUnitActionQueue(*u, mapPoints, numPoints05); processUnitActionQueue(*u, mapPoints, numPoints05);
} }
} }
} }
}
// Clean up dead units // Clean up dead units
for (auto it = m_units.begin(); it != m_units.end(); ) { for (auto it = m_units.begin(); it != m_units.end(); ) {
@@ -162,6 +170,9 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int
++it; ++it;
} }
} }
// Update Action Needed Queue for active human player 1
m_actionNeededManager.update(deltaTime, 1, m_units, m_cities);
} }
Unit* GameManager::getSelectedUnit() Unit* GameManager::getSelectedUnit()
@@ -663,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));
@@ -1032,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;
@@ -1054,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;
} }
+6 -1
View File
@@ -13,7 +13,7 @@
#include "matrix2d.h" #include "matrix2d.h"
#include "terrain.h" #include "terrain.h"
#include "terrainyieldmanager.h" #include "terrainyieldmanager.h"
#include "roadconfig.h" #include "actionneededmanager.h"
enum class ActionMode { enum class ActionMode {
None, None,
@@ -70,6 +70,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; }
@@ -119,6 +121,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);
@@ -161,6 +165,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;
+225
View File
@@ -2267,6 +2267,29 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event)
return; // In City View, ignore all other clicks 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 // Check if clicked on a City Banner to open City View
for (const auto &cb : m_cityBannerHits) { for (const auto &cb : m_cityBannerHits) {
if (cb.rect.contains(event->pos())) { if (cb.rect.contains(event->pos())) {
@@ -2478,6 +2501,20 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
if (zRot >= 360.0f) zRot -= 360.0f; if (zRot >= 360.0f) zRot -= 360.0f;
update(); 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(); lastPos = event->pos();
} }
@@ -2783,6 +2820,7 @@ void QSceneRenderer::paintGL()
if (selected) { if (selected) {
drawUnitCard(painter, selected); drawUnitCard(painter, selected);
} }
drawActionNeededQueue(painter);
} else { } else {
drawUnitsOverlay(painter, vMatrix); drawUnitsOverlay(painter, vMatrix);
drawFloatingTexts(painter, vMatrix); drawFloatingTexts(painter, vMatrix);
@@ -2790,3 +2828,190 @@ void QSceneRenderer::paintGL()
} }
painter.end(); 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
View File
@@ -32,7 +32,8 @@ SOURCES += main.cpp \
unitrenderer.cpp \ unitrenderer.cpp \
localization.cpp \ localization.cpp \
terrainmovementmanager.cpp \ terrainmovementmanager.cpp \
roadconfig.cpp roadconfig.cpp \
actionneededmanager.cpp
HEADERS += mainwindow.h \ HEADERS += mainwindow.h \
qscenerenderer.h \ qscenerenderer.h \
@@ -54,7 +55,8 @@ HEADERS += mainwindow.h \
unitrenderer.h \ unitrenderer.h \
localization.h \ localization.h \
terrainmovementmanager.h \ terrainmovementmanager.h \
roadconfig.h roadconfig.h \
actionneededmanager.h
FORMS += mainwindow.ui FORMS += mainwindow.ui