diff --git a/src/ui/SelectedBuildingPanel.cpp b/src/ui/SelectedBuildingPanel.cpp index 5c6f79e..84bc502 100644 --- a/src/ui/SelectedBuildingPanel.cpp +++ b/src/ui/SelectedBuildingPanel.cpp @@ -9,26 +9,14 @@ #include #include #include -#include #include #include "BeltSystem.h" #include "Command.h" #include "CommandRequestedEvent.h" -#include "DisplayName.h" -#include "DynamicBodyComponent.h" -#include "EntityAdmin.h" #include "EntitySelectionChangedEvent.h" #include "EventManager.h" -#include "FactionComponent.h" -#include "HealthComponent.h" -#include "ModuleOwnerComponent.h" -#include "SelectedBehaviorComponent.h" -#include "ShipIdentityComponent.h" -#include "ShipStatsCalculator.h" -#include "ShipStatsPanel.h" -#include "ThreatCostCalculator.h" -#include "StationBodyComponent.h" +#include "FieldSelectionPanel.h" #include "TickAdvancedEvent.h" #include "Building.h" #include "BuildingSystem.h" @@ -40,10 +28,8 @@ #include "RecipeSelectionDialog.h" #include "RecipeSelectionRequestedEvent.h" #include "Rotation.h" -#include "DebrisSystem.h" #include "ShipLayoutPreview.h" #include "Simulation.h" -#include "WeaponComponent.h" namespace { @@ -182,30 +168,10 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim, connect(m_filterBList, &QListWidget::itemChanged, this, &SelectedBuildingPanel::onSplitterFilterChanged); - m_entityTitleLabel = new QLabel(this); - QFont titleFont = m_entityTitleLabel->font(); - titleFont.setBold(true); - m_entityTitleLabel->setFont(titleFont); - m_layout->addWidget(m_entityTitleLabel); - m_entityTitleLabel->hide(); - - m_entityStatsPanel = new ShipStatsPanel(config, this); - m_layout->addWidget(m_entityStatsPanel); - m_entityStatsPanel->hide(); - - m_stationStatsLabel = new QLabel(this); - m_stationStatsLabel->setWordWrap(true); - 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(); + // The field selection renders below the building content and hides itself while + // nothing field-side is selected, so it costs no space then. + m_fieldSelectionPanel = new FieldSelectionPanel(sim, config, this); + m_layout->addWidget(m_fieldSelectionPanel); buildEmpty(); @@ -224,13 +190,21 @@ void SelectedBuildingPanel::onSelectionChanged(const std::vector& id { // A building selection is exclusive: it supersedes any field selection — // actors and scrap (REQ-UI-SELECTION-CATEGORIES). - clearEntityDisplay(); - m_selectedDebris.clear(); - m_scrapLabel->hide(); + m_fieldSelectionPanel->clearSelection(); } rebuild(); } +void SelectedBuildingPanel::yieldToFieldSelection() +{ + // The mirror image of onSelectionChanged(): a field selection — actors, debris, or + // both — supersedes any building selection (REQ-UI-SELECTION-CATEGORIES). An empty + // field selection changes nothing here: the building content, if any, keeps the panel. + if (!m_fieldSelectionPanel->hasSelection()) { return; } + m_selectedBuildingIds.clear(); + buildEmpty(); +} + void SelectedBuildingPanel::rebuild() { if (m_selectedBuildingIds.empty()) @@ -259,22 +233,14 @@ void SelectedBuildingPanel::hideAllWidgets() m_filterBLabel->hide(); m_filterBList->hide(); m_buffersLabel->hide(); - m_scrapLabel->hide(); -} - -void SelectedBuildingPanel::clearContent() -{ - m_singleBuildingId = std::nullopt; - hideAllWidgets(); } void SelectedBuildingPanel::buildEmpty() { - clearContent(); - m_entityTitleLabel->hide(); - m_entityStatsPanel->hide(); - m_stationStatsLabel->hide(); - m_entitySummaryLabel->hide(); + // Shows nothing for the building category — either because nothing is selected or + // because the field category has taken the panel over. + m_singleBuildingId = std::nullopt; + hideAllWidgets(); } void SelectedBuildingPanel::buildSingle(BuildingId id) @@ -646,27 +612,9 @@ void SelectedBuildingPanel::handleEvent( void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason) { - if (!m_selectedEntities.empty() || !m_selectedDebris.empty()) - { - // Field selection. Keep the live values current: the single-actor stats panel, - // the single-debris stats panel (whose Scrap row shrinks as it is collected), or - // the count summary (whose Scrap line shrinks likewise) — matching the layout - // chosen by buildFieldSelection() (REQ-UI-SHIP-STATS-PANEL, REQ-UI-DEBRIS-PANEL). - if (m_selectedEntities.size() == 1 && m_selectedDebris.empty()) - { - refreshEntityStats(); - } - else if (m_selectedEntities.empty() && m_selectedDebris.size() == 1) - { - buildDebrisSingle(); - } - else - { - buildEntitySummary(); - } - return; - } - + // Only a single selected building has live content to refresh. While the field + // category owns the panel there is none: yieldToFieldSelection() has cleared it, so + // this returns immediately and the field panel refreshes itself off the same events. if (!m_singleBuildingId.has_value()) { return; } const Building* b = m_sim->getBuildings().findBuilding(*m_singleBuildingId); if (b) @@ -904,259 +852,8 @@ void SelectedBuildingPanel::onClearBelt() void SelectedBuildingPanel::handleEvent(std::shared_ptr event) { - m_selectedEntities = event->entities; - if (!m_selectedEntities.empty()) - { - // A field selection supersedes any building selection (REQ-UI-SELECTION-CATEGORIES). - m_selectedBuildingIds.clear(); - } - buildFieldSelection(); -} - -void SelectedBuildingPanel::buildFieldSelection() -{ - if (m_selectedEntities.empty() && m_selectedDebris.empty()) - { - // Nothing in the field category. Fall back to empty unless buildings own the panel. - clearEntityDisplay(); - m_scrapLabel->hide(); - if (m_selectedBuildingIds.empty()) - { - buildEmpty(); - } - return; - } - - // A field selection owns the panel: drop any building content. - clearContent(); - - EntityAdmin& admin = m_sim->getAdmin(); - - // A full single-object stats panel is shown only for a lone field object: one actor - // with no debris, or one piece of debris with no actors. As soon as the selection holds - // more than one object (multiple actors, multiple debris, or actors plus debris), the - // panel switches to the compact count summary (REQ-UI-FIELD-MULTI-SELECTION). - if (m_selectedEntities.size() == 1 && m_selectedDebris.empty()) - { - m_entitySummaryLabel->hide(); - m_scrapLabel->hide(); - const entt::entity entity = m_selectedEntities.front(); - if (admin.isValid(entity) && admin.hasAll(entity)) - { - buildEntityShip(entity); - } - else if (admin.isValid(entity) && admin.hasAll(entity)) - { - buildEntityStation(entity); - } - else - { - m_entityTitleLabel->hide(); - m_entityStatsPanel->hide(); - m_stationStatsLabel->hide(); - } - return; - } - - if (m_selectedEntities.empty() && m_selectedDebris.size() == 1) - { - // Single piece of debris: a "Debris" heading plus a "Scrap" stat row, styled like - // the ship/station stats panels (REQ-UI-DEBRIS-PANEL). - m_entitySummaryLabel->hide(); - m_entityStatsPanel->hide(); - m_stationStatsLabel->hide(); - buildDebrisSingle(); - return; - } - - // More than one field object: a compact count summary. buildEntitySummary() appends the - // "Debris x N" and "Scrap x N" lines when debris is part of the selection. - m_entityTitleLabel->hide(); - m_entityStatsPanel->hide(); - m_stationStatsLabel->hide(); - m_scrapLabel->hide(); - buildEntitySummary(); -} - -void SelectedBuildingPanel::buildDebrisSingle() -{ - // "Debris" heading + a single "Scrap" stat row for the piece's remaining amount, - // mirroring the single-actor stats panels (REQ-UI-DEBRIS-PANEL). - m_entityTitleLabel->setText(tr("Debris")); - m_entityTitleLabel->show(); - m_scrapLabel->setText(tr("Scrap: %1").arg(selectedDebrisScrapTotal())); - m_scrapLabel->show(); -} - -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 keys; - std::map counts; - std::map labels; - - for (entt::entity entity : m_selectedEntities) - { - if (!admin.isValid(entity)) { continue; } - const bool isEnemy = admin.hasAll(entity) - && admin.get(entity).isEnemy; - - QString key; - QString label; - if (admin.hasAll(entity)) - { - const std::string& id = admin.get(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(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 " x " line per group (matching the recipe tooltip and the building - // multi-selection). No total-count header, consistent with the building panel. When - // debris is part of the selection, a "Debris x " line followed by a - // "Scrap x " line are appended into the same label so the line spacing is - // uniform (REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-DEBRIS-PANEL). - QStringList lines; - for (const QString& key : keys) - { - lines << tr("%1 x %2").arg(labels[key]).arg(counts[key]); - } - if (!m_selectedDebris.empty()) - { - lines << tr("Debris x %1").arg(static_cast(m_selectedDebris.size())); - lines << scrapTotalText(); - } - m_entitySummaryLabel->setText(lines.join('\n')); - m_entitySummaryLabel->show(); -} - -void SelectedBuildingPanel::buildEntityShip(entt::entity entity) -{ - EntityAdmin& admin = m_sim->getAdmin(); - const ShipIdentityComponent& identity = admin.get(entity); - const HealthComponent& health = admin.get(entity); - - m_entityTitleLabel->setText(tr("Ship: %1") - .arg(QString::fromStdString(identity.schematicId))); - m_entityTitleLabel->show(); - - const ShipStats stats = buildShipStatsFromEntity(admin, entity); - m_entityStatsPanel->refreshFromLive(stats, health.hp); - m_entityStatsPanel->setBehavior( - admin.get(entity).winner); - m_entityStatsPanel->setDebugDrawEnabled(m_debugDraw); - - const ShipDef* schematicDef = - m_config->ships.findShipDef(identity.schematicId); - if (schematicDef) - { - const double threat = calculateShipThreatCost( - m_config->threatCosts, *m_config, schematicDef->id, - schematicDef->defaultModules); - m_entityStatsPanel->setThreatCost(threat); - } - - m_entityStatsPanel->show(); - - m_stationStatsLabel->hide(); -} - -void SelectedBuildingPanel::buildEntityStation(entt::entity entity) -{ - EntityAdmin& admin = m_sim->getAdmin(); - const HealthComponent& health = admin.get(entity); - - const bool isEnemy = admin.hasAll(entity) - && admin.get(entity).isEnemy; - m_entityTitleLabel->setText(isEnemy - ? tr("Enemy Defence Station") - : tr("Player Defence Station")); - m_entityTitleLabel->show(); - - float totalDps = 0.0f; - float maxRange = 0.0f; - bool hasWeapons = false; - - admin.forEach( - [&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w) - { - if (owner.owner != entity) { return; } - hasWeapons = true; - totalDps += w.damage * w.fireRateHz; - if (w.range_tiles > maxRange) { maxRange = w.range_tiles; } - }); - - QString statsText = tr("HP: %1 / %2") - .arg(static_cast(health.hp + 0.5f)) - .arg(static_cast(health.maxHp + 0.5f)); - - if (hasWeapons) - { - statsText += tr("\nDPS: %1").arg(QString::number(static_cast(totalDps), 'f', 1)); - statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast(maxRange), 'f', 1)); - } - - m_stationStatsLabel->setText(statsText); - m_stationStatsLabel->show(); - - m_entityStatsPanel->hide(); -} - -void SelectedBuildingPanel::refreshEntityStats() -{ - // 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(); - const entt::entity entity = m_selectedEntities.front(); - - if (!admin.isValid(entity) || !admin.hasAll(entity)) { return; } - const HealthComponent& health = admin.get(entity); - if (health.hp <= 0.0f) { return; } - - if (admin.hasAll(entity)) - { - const ShipStats stats = buildShipStatsFromEntity(admin, entity); - m_entityStatsPanel->refreshFromLive(stats, health.hp); - m_entityStatsPanel->setBehavior( - admin.get(entity).winner); - } - else if (admin.hasAll(entity)) - { - buildEntityStation(entity); - } -} - -void SelectedBuildingPanel::clearEntityDisplay() -{ - m_selectedEntities.clear(); - m_entityTitleLabel->hide(); - m_entityStatsPanel->hide(); - m_stationStatsLabel->hide(); - m_entitySummaryLabel->hide(); + m_fieldSelectionPanel->setSelectedEntities(event->entities); + yieldToFieldSelection(); } void SelectedBuildingPanel::handleEvent(std::shared_ptr event) @@ -1167,38 +864,8 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr event) { - m_selectedDebris = event->debris; - if (!m_selectedDebris.empty()) - { - // Debris is a field object: it supersedes any building selection but coexists - // with actors (REQ-UI-SELECTION-CATEGORIES). - m_selectedBuildingIds.clear(); - } - buildFieldSelection(); -} - -int SelectedBuildingPanel::selectedDebrisScrapTotal() const -{ - // Sum the remaining scrap across the still-living selected debris (REQ-UI-DEBRIS-PANEL). - int total = 0; - for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo()) - { - if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity) - != m_selectedDebris.end()) - { - total += info.amount; - } - } - return total; -} - -QString SelectedBuildingPanel::scrapTotalText() const -{ - return tr("Scrap x %1").arg(selectedDebrisScrapTotal()); -} - -void SelectedBuildingPanel::handleEvent(std::shared_ptr event) -{ - m_debugDraw = event->active; - m_entityStatsPanel->setDebugDrawEnabled(event->active); + // Debris is a field object: it supersedes any building selection but coexists + // with actors (REQ-UI-SELECTION-CATEGORIES). + m_fieldSelectionPanel->setSelectedDebris(event->debris); + yieldToFieldSelection(); } diff --git a/src/ui/SelectedBuildingPanel.h b/src/ui/SelectedBuildingPanel.h index f2b6c4f..244442b 100644 --- a/src/ui/SelectedBuildingPanel.h +++ b/src/ui/SelectedBuildingPanel.h @@ -7,12 +7,9 @@ #include #include -#include "entt/entity/entity.hpp" - #include "BeltSystem.h" #include "Building.h" #include "BuildingId.h" -#include "DebugDrawToggledEvent.h" #include "EntitySelectionChangedEvent.h" #include "EventHandler.h" #include "GameConfig.h" @@ -26,20 +23,27 @@ #include "TickAdvancedEvent.h" class Simulation; +class FieldSelectionPanel; class ShipLayoutPreview; -class ShipStatsPanel; class QLabel; class QListWidget; class QPushButton; class QVBoxLayout; +// Shows the current selection. The building category (buildings and construction sites) +// is rendered by this panel itself; the field category (ships, defence stations, debris) +// is rendered by the embedded FieldSelectionPanel. +// +// The two categories are mutually exclusive (REQ-UI-SELECTION-CATEGORIES) and this panel +// is the sole arbiter of which one owns the content: it listens to all three selection +// events, forwards the field ones to the child panel, and drops the losing category's +// content. Neither panel touches the other's widgets. class SelectedBuildingPanel : public QWidget, public CombinedEventHandler + DebrisSelectionChangedEvent> { Q_OBJECT @@ -54,7 +58,6 @@ private: void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; - void handleEvent(std::shared_ptr event) override; private slots: void onSelectRecipeClicked(); @@ -73,17 +76,14 @@ private: }; void onSelectionChanged(const std::vector& ids); + // Gives the panel to the field category once it has anything selected. + void yieldToFieldSelection(); void refreshSelectionDisplay(RefreshReason reason); void rebuild(); void hideAllWidgets(); - void clearContent(); void buildEmpty(); void buildSingle(BuildingId id); void buildMulti(const std::vector& ids); - // Summed remaining scrap across the selected debris (REQ-UI-DEBRIS-PANEL). - int selectedDebrisScrapTotal() const; - // "Scrap x N" line for the multi-object summary (REQ-UI-FIELD-MULTI-SELECTION). - QString scrapTotalText() const; void refreshBuffers(const Building* b); void refreshSiteProgress(const ConstructionSite* s); void updateShipyardLayoutWidgets(BuildingType type, @@ -116,28 +116,7 @@ private: QPoint m_splitterTile; std::string m_currentRecipeId; - bool m_debugDraw = false; - // The selected ships/defence stations. Shares the "field" selection category with - // debris (m_selectedDebris): both can be non-empty at once (REQ-UI-SELECTION-CATEGORIES). - std::vector m_selectedEntities; - ShipStatsPanel* m_entityStatsPanel; - QLabel* m_entityTitleLabel; - QLabel* m_stationStatsLabel; - QLabel* m_entitySummaryLabel; - - std::vector m_selectedDebris; - // Shows the debris "Scrap" stat row (single selection) — the scrap total for the - // multi-object summary lives in m_entitySummaryLabel instead. - QLabel* m_scrapLabel; - - // Renders the combined field selection (actors + debris): a single-object stats panel - // (ship, station, or debris) or a multi-object count summary that appends the debris - // count and scrap total when debris is also selected (REQ-UI-FIELD-MULTI-SELECTION). - void buildFieldSelection(); - void buildEntityShip(entt::entity entity); - void buildEntityStation(entt::entity entity); - void buildEntitySummary(); - void buildDebrisSingle(); - void refreshEntityStats(); - void clearEntityDisplay(); + // Renders the field selection (actors + debris) below the building content + // (REQ-UI-FIELD-MULTI-SELECTION). Hides itself while nothing field-side is selected. + FieldSelectionPanel* m_fieldSelectionPanel; };