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;
}