allow multi-select for ships/stations, mixable with scrap
This commit is contained in:
@@ -9,14 +9,16 @@
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QStringList>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BeltSystem.h"
|
||||
#include "Command.h"
|
||||
#include "CommandRequestedEvent.h"
|
||||
#include "DisplayName.h"
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "EntitySelectedEvent.h"
|
||||
#include "EntitySelectionChangedEvent.h"
|
||||
#include "EventManager.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
@@ -196,6 +198,11 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
||||
m_layout->addWidget(m_stationStatsLabel);
|
||||
m_stationStatsLabel->hide();
|
||||
|
||||
m_entitySummaryLabel = new QLabel(this);
|
||||
m_entitySummaryLabel->setWordWrap(true);
|
||||
m_layout->addWidget(m_entitySummaryLabel);
|
||||
m_entitySummaryLabel->hide();
|
||||
|
||||
m_scrapLabel = new QLabel(this);
|
||||
m_layout->addWidget(m_scrapLabel);
|
||||
m_scrapLabel->hide();
|
||||
@@ -215,8 +222,9 @@ void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& id
|
||||
m_selectedBuildingIds = ids;
|
||||
if (!ids.empty())
|
||||
{
|
||||
// A building selection is exclusive: it supersedes any field selection —
|
||||
// actors and scrap (REQ-UI-SELECTION-CATEGORIES).
|
||||
clearEntityDisplay();
|
||||
// A building selection supersedes any scrap selection (REQ-UI-SCRAP-CLICK-SELECT).
|
||||
m_selectedScrap.clear();
|
||||
m_scrapLabel->hide();
|
||||
}
|
||||
@@ -266,6 +274,7 @@ void SelectedBuildingPanel::buildEmpty()
|
||||
m_entityTitleLabel->hide();
|
||||
m_entityStatsPanel->hide();
|
||||
m_stationStatsLabel->hide();
|
||||
m_entitySummaryLabel->hide();
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::buildSingle(BuildingId id)
|
||||
@@ -658,16 +667,24 @@ void SelectedBuildingPanel::handleEvent(
|
||||
|
||||
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
|
||||
{
|
||||
if (!m_selectedScrap.empty())
|
||||
if (!m_selectedEntities.empty() || !m_selectedScrap.empty())
|
||||
{
|
||||
// The total shrinks live as piles are collected or despawn (REQ-UI-SCRAP-PANEL).
|
||||
refreshScrapTotal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_selectedEntity.has_value())
|
||||
{
|
||||
refreshEntityStats();
|
||||
// Field selection. Keep the live values current: the single-actor stats panel,
|
||||
// the standalone scrap total, or the count summary (whose scrap line shrinks as
|
||||
// piles are collected) — matching the layout chosen by buildFieldSelection()
|
||||
// (REQ-UI-SHIP-STATS-PANEL, REQ-UI-SCRAP-PANEL).
|
||||
if (m_selectedEntities.size() == 1 && m_selectedScrap.empty())
|
||||
{
|
||||
refreshEntityStats();
|
||||
}
|
||||
else if (m_selectedEntities.empty())
|
||||
{
|
||||
refreshScrapTotal();
|
||||
}
|
||||
else
|
||||
{
|
||||
buildEntitySummary();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -737,7 +754,7 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
|
||||
QString text;
|
||||
for (const std::pair<const BuildingType, int>& entry : counts)
|
||||
{
|
||||
text += buildingTypeName(entry.first) + ": "
|
||||
text += buildingTypeName(entry.first) + " x "
|
||||
+ QString::number(entry.second) + "\n";
|
||||
if (isBeltLike(entry.first))
|
||||
{
|
||||
@@ -906,39 +923,139 @@ void SelectedBuildingPanel::onClearBelt()
|
||||
}
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectedEvent> event)
|
||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event)
|
||||
{
|
||||
if (event->entity.has_value())
|
||||
m_selectedEntities = event->entities;
|
||||
if (!m_selectedEntities.empty())
|
||||
{
|
||||
m_selectedEntity = event->entity;
|
||||
// A field selection supersedes any building selection (REQ-UI-SELECTION-CATEGORIES).
|
||||
m_selectedBuildingIds.clear();
|
||||
// An entity selection supersedes any scrap selection (REQ-UI-SCRAP-CLICK-SELECT).
|
||||
m_selectedScrap.clear();
|
||||
}
|
||||
buildFieldSelection();
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::buildFieldSelection()
|
||||
{
|
||||
if (m_selectedEntities.empty() && m_selectedScrap.empty())
|
||||
{
|
||||
// Nothing in the field category. Fall back to empty unless buildings own the panel.
|
||||
clearEntityDisplay();
|
||||
m_scrapLabel->hide();
|
||||
clearContent();
|
||||
|
||||
EntityAdmin& admin = m_sim->getAdmin();
|
||||
entt::entity entity = *m_selectedEntity;
|
||||
|
||||
if (!admin.isValid(entity))
|
||||
if (m_selectedBuildingIds.empty())
|
||||
{
|
||||
clearEntityDisplay();
|
||||
return;
|
||||
buildEmpty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (admin.hasAll<ShipIdentityComponent>(entity))
|
||||
// A field selection owns the panel: drop any building content.
|
||||
clearContent();
|
||||
|
||||
EntityAdmin& admin = m_sim->getAdmin();
|
||||
|
||||
// Full single-actor stats are shown only for a lone actor with no scrap. As soon as
|
||||
// the selection holds more than one object (multiple actors, or an actor plus scrap),
|
||||
// the panel switches to the compact count summary (REQ-UI-FIELD-MULTI-SELECTION).
|
||||
if (m_selectedEntities.size() == 1 && m_selectedScrap.empty())
|
||||
{
|
||||
m_entitySummaryLabel->hide();
|
||||
m_scrapLabel->hide();
|
||||
const entt::entity entity = m_selectedEntities.front();
|
||||
if (admin.isValid(entity) && admin.hasAll<ShipIdentityComponent>(entity))
|
||||
{
|
||||
buildEntityShip(entity);
|
||||
}
|
||||
else if (admin.hasAll<StationBodyComponent>(entity))
|
||||
else if (admin.isValid(entity) && admin.hasAll<StationBodyComponent>(entity))
|
||||
{
|
||||
buildEntityStation(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_entityTitleLabel->hide();
|
||||
m_entityStatsPanel->hide();
|
||||
m_stationStatsLabel->hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
m_entityTitleLabel->hide();
|
||||
m_entityStatsPanel->hide();
|
||||
m_stationStatsLabel->hide();
|
||||
|
||||
if (m_selectedEntities.empty())
|
||||
{
|
||||
clearEntityDisplay();
|
||||
// Scrap only: a single "Scrap: N" line.
|
||||
m_entitySummaryLabel->hide();
|
||||
refreshScrapTotal();
|
||||
m_scrapLabel->show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Actor counts, with the scrap total appended into the same label so every line
|
||||
// shares the same spacing.
|
||||
m_scrapLabel->hide();
|
||||
buildEntitySummary();
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::buildEntitySummary()
|
||||
{
|
||||
EntityAdmin& admin = m_sim->getAdmin();
|
||||
|
||||
// Group actors by faction + kind + ship schematic, preserving first-seen order
|
||||
// (REQ-UI-FIELD-MULTI-SELECTION).
|
||||
std::vector<QString> keys;
|
||||
std::map<QString, int> counts;
|
||||
std::map<QString, QString> labels;
|
||||
|
||||
for (entt::entity entity : m_selectedEntities)
|
||||
{
|
||||
if (!admin.isValid(entity)) { continue; }
|
||||
const bool isEnemy = admin.hasAll<FactionComponent>(entity)
|
||||
&& admin.get<FactionComponent>(entity).isEnemy;
|
||||
|
||||
QString key;
|
||||
QString label;
|
||||
if (admin.hasAll<ShipIdentityComponent>(entity))
|
||||
{
|
||||
const std::string& id = admin.get<ShipIdentityComponent>(entity).schematicId;
|
||||
const QString name = QString::fromStdString(toDisplayName(id));
|
||||
key = (isEnemy ? QStringLiteral("ship:enemy:") : QStringLiteral("ship:player:"))
|
||||
+ QString::fromStdString(id);
|
||||
label = isEnemy ? tr("Enemy %1").arg(name) : name;
|
||||
}
|
||||
else if (admin.hasAll<StationBodyComponent>(entity))
|
||||
{
|
||||
key = isEnemy ? QStringLiteral("station:enemy") : QStringLiteral("station:player");
|
||||
label = isEnemy ? tr("Enemy Defence Station") : tr("Player Defence Station");
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (counts.find(key) == counts.end())
|
||||
{
|
||||
keys.push_back(key);
|
||||
labels[key] = label;
|
||||
}
|
||||
counts[key] += 1;
|
||||
}
|
||||
|
||||
// One "<type> x <count>" line per group (matching the recipe tooltip and the building
|
||||
// multi-selection). No total-count header, consistent with the building panel. The
|
||||
// scrap total, when present, is appended as another line in the same label so the
|
||||
// line spacing is uniform (REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-SCRAP-PANEL).
|
||||
QStringList lines;
|
||||
for (const QString& key : keys)
|
||||
{
|
||||
lines << tr("%1 x %2").arg(labels[key]).arg(counts[key]);
|
||||
}
|
||||
if (!m_selectedScrap.empty())
|
||||
{
|
||||
lines << scrapTotalText();
|
||||
}
|
||||
m_entitySummaryLabel->setText(lines.join('\n'));
|
||||
m_entitySummaryLabel->show();
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
|
||||
@@ -1016,23 +1133,17 @@ void SelectedBuildingPanel::buildEntityStation(entt::entity entity)
|
||||
|
||||
void SelectedBuildingPanel::refreshEntityStats()
|
||||
{
|
||||
if (!m_selectedEntity.has_value()) { return; }
|
||||
// Only the single-actor stats panel needs a live refresh; the multi-actor summary is
|
||||
// static counts, and GameWorldView prunes dead/despawned actors and re-emits the
|
||||
// selection (REQ-UI-ENTITY-CLICK-SELECT), so the panel does not mutate it here.
|
||||
if (m_selectedEntities.size() != 1) { return; }
|
||||
|
||||
EntityAdmin& admin = m_sim->getAdmin();
|
||||
entt::entity entity = *m_selectedEntity;
|
||||
|
||||
if (!admin.isValid(entity))
|
||||
{
|
||||
clearEntityDisplay();
|
||||
return;
|
||||
}
|
||||
const entt::entity entity = m_selectedEntities.front();
|
||||
|
||||
if (!admin.isValid(entity) || !admin.hasAll<HealthComponent>(entity)) { return; }
|
||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||
if (health.hp <= 0.0f)
|
||||
{
|
||||
clearEntityDisplay();
|
||||
return;
|
||||
}
|
||||
if (health.hp <= 0.0f) { return; }
|
||||
|
||||
if (admin.hasAll<ShipIdentityComponent>(entity))
|
||||
{
|
||||
@@ -1049,10 +1160,11 @@ void SelectedBuildingPanel::refreshEntityStats()
|
||||
|
||||
void SelectedBuildingPanel::clearEntityDisplay()
|
||||
{
|
||||
m_selectedEntity = std::nullopt;
|
||||
m_selectedEntities.clear();
|
||||
m_entityTitleLabel->hide();
|
||||
m_entityStatsPanel->hide();
|
||||
m_stationStatsLabel->hide();
|
||||
m_entitySummaryLabel->hide();
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||
@@ -1066,34 +1178,14 @@ void SelectedBuildingPanel::handleEvent(
|
||||
m_selectedScrap = event->scrap;
|
||||
if (!m_selectedScrap.empty())
|
||||
{
|
||||
// Scrap is its own selection category, mutually exclusive with buildings and
|
||||
// entities (REQ-UI-SCRAP-CLICK-SELECT).
|
||||
// Scrap is a field object: it supersedes any building selection but coexists
|
||||
// with actors (REQ-UI-SELECTION-CATEGORIES).
|
||||
m_selectedBuildingIds.clear();
|
||||
clearContent();
|
||||
clearEntityDisplay();
|
||||
buildScrap();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_scrapLabel->hide();
|
||||
if (m_selectedBuildingIds.empty() && !m_selectedEntity.has_value())
|
||||
{
|
||||
buildEmpty();
|
||||
}
|
||||
}
|
||||
buildFieldSelection();
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::buildScrap()
|
||||
{
|
||||
clearContent();
|
||||
m_entityTitleLabel->hide();
|
||||
m_entityStatsPanel->hide();
|
||||
m_stationStatsLabel->hide();
|
||||
refreshScrapTotal();
|
||||
m_scrapLabel->show();
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::refreshScrapTotal()
|
||||
QString SelectedBuildingPanel::scrapTotalText() const
|
||||
{
|
||||
// Sum the remaining amounts of the still-living selected piles (REQ-UI-SCRAP-PANEL).
|
||||
int total = 0;
|
||||
@@ -1105,7 +1197,12 @@ void SelectedBuildingPanel::refreshScrapTotal()
|
||||
total += info.amount;
|
||||
}
|
||||
}
|
||||
m_scrapLabel->setText(tr("Scrap: %1").arg(total));
|
||||
return tr("Scrap x %1").arg(total);
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::refreshScrapTotal()
|
||||
{
|
||||
m_scrapLabel->setText(scrapTotalText());
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)
|
||||
|
||||
Reference in New Issue
Block a user