diff --git a/actionneededmanager.cpp b/actionneededmanager.cpp new file mode 100644 index 0000000..f5d314f --- /dev/null +++ b/actionneededmanager.cpp @@ -0,0 +1,243 @@ +#include "actionneededmanager.h" +#include "yamlparser.h" +#include "unit.h" +#include "city.h" +#include +#include +#include + +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> &units, + const std::vector &cities) +{ + if (!m_config.enabled) return; + + m_elapsedGameTime += static_cast(deltaTime); + + std::set 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(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(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(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 ActionNeededManager::getVisibleItems() const +{ + std::vector 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; +} diff --git a/actionneededmanager.h b/actionneededmanager.h new file mode 100644 index 0000000..896736f --- /dev/null +++ b/actionneededmanager.h @@ -0,0 +1,79 @@ +#ifndef ACTIONNEEDEDMANAGER_H +#define ACTIONNEEDEDMANAGER_H + +#include +#include +#include +#include + +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(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> &units, + const std::vector &cities); + + std::vector 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 m_items; + double m_elapsedGameTime; +}; + +#endif // ACTIONNEEDEDMANAGER_H diff --git a/config/action_needed.yaml b/config/action_needed.yaml new file mode 100644 index 0000000..1e6bff0 --- /dev/null +++ b/config/action_needed.yaml @@ -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 diff --git a/config/strings_en.yaml b/config/strings_en.yaml index 4e4bf3d..e2c2133 100644 --- a/config/strings_en.yaml +++ b/config/strings_en.yaml @@ -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" + diff --git a/gamemanager.cpp b/gamemanager.cpp index 10cf386..c059a50 100644 --- a/gamemanager.cpp +++ b/gamemanager.cpp @@ -58,6 +58,10 @@ void GameManager::init(const QString &baseDir, Matrix2D *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 *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 *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 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 *mapPoints, in selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f); } +void GameManager::commandBuildRoadToTileSelected(int targetX, int targetY, Matrix2D *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> 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 *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 *mapPoints, int numPoints05) { int tx = unit.tileX; @@ -1054,6 +1146,11 @@ void GameManager::completeUnitBuilding(Unit &unit, Matrix2D *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; } diff --git a/gamemanager.h b/gamemanager.h index 7d6cc2a..cb427a7 100644 --- a/gamemanager.h +++ b/gamemanager.h @@ -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>& getUnits() const { return m_units; } const std::vector& getCities() const { return m_cities; } @@ -119,6 +121,8 @@ public: void commandBuildCitySelected(Matrix2D *mapPoints, int numPoints05); void commandBuildFarmSelected(Matrix2D *mapPoints, int numPoints05); void commandBuildRoadSelected(Matrix2D *mapPoints, int numPoints05); + void commandBuildRoadToTileSelected(int targetX, int targetY, Matrix2D *mapPoints, int numPoints05); + void advanceRoadRoute(Unit &unit, Matrix2D *mapPoints, int numPoints05); void completeUnitBuilding(Unit &unit, Matrix2D *mapPoints, int numPoints05); void processUnitActionQueue(Unit &unit, Matrix2D *mapPoints, int numPoints05); @@ -161,6 +165,7 @@ private: UnitConfigManager m_unitConfigManager; BuildingConfigManager m_buildingConfigManager; RoadConfigManager m_roadConfigManager; + ActionNeededManager m_actionNeededManager; std::vector> m_units; std::vector m_cities; std::vector m_floatingTexts; diff --git a/qscenerenderer.cpp b/qscenerenderer.cpp index 8d5c202..7b2b271 100644 --- a/qscenerenderer.cpp +++ b/qscenerenderer.cpp @@ -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 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; + } + } + } +} diff --git a/tiletest.pro b/tiletest.pro index a898e55..64493f8 100644 --- a/tiletest.pro +++ b/tiletest.pro @@ -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