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
+225
View File
@@ -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;
}
}
}
}