From b97ab1edaaf01cef71748aba39c4e806a443b4c2 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Mon, 13 Jul 2026 20:58:28 +0200 Subject: [PATCH] Implement scrap pile selection Scrap piles become a third selection category: click to select (actors win on overlap), box-drag / Ctrl+click multi-select, and the selected- object panel shows the summed remaining amount, updating live and dropping piles as they are collected or despawn. - ScrapSystem: expose per-pile amount via ScrapInfo. - EntityHitTest: add scrapAtWorldPos and scrapInBox (with tests). - New header-only ScrapSelectionChangedEvent. - GameWorldView: scrap selection member, click/box handling with the building-wins disambiguation, and per-frame pruning of gone piles. - SelectedBuildingPanel: scrap event, "Scrap: N" label, live total; scrap and building/entity selections clear each other. Implements REQ-UI-SCRAP-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT, REQ-UI-SCRAP-PANEL. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DZR44tA8sn4dPqDzAVXyps --- src/lib/ecs/system/ScrapSystem.cpp | 4 +- src/lib/ecs/system/ScrapSystem.h | 1 + .../event/ScrapSelectionChangedEvent.h | 18 +++ src/lib/sim/EntityHitTest.cpp | 47 ++++++ src/lib/sim/EntityHitTest.h | 12 ++ src/test/ScrapTest.cpp | 86 ++++++++++ src/ui/GameWorldView.cpp | 149 ++++++++++++++++-- src/ui/GameWorldView.h | 8 + src/ui/SelectedBuildingPanel.cpp | 67 ++++++++ src/ui/SelectedBuildingPanel.h | 8 + 10 files changed, 384 insertions(+), 16 deletions(-) create mode 100644 src/lib/eventsystem/event/ScrapSelectionChangedEvent.h diff --git a/src/lib/ecs/system/ScrapSystem.cpp b/src/lib/ecs/system/ScrapSystem.cpp index 0a31713..f6c1463 100644 --- a/src/lib/ecs/system/ScrapSystem.cpp +++ b/src/lib/ecs/system/ScrapSystem.cpp @@ -69,9 +69,9 @@ std::vector ScrapSystem::allScrapInfo() const { std::vector result; m_admin.forEach( - [&result, this](entt::entity e, const ScrapDataComponent& /*sd*/) + [&result, this](entt::entity e, const ScrapDataComponent& sd) { - result.push_back(ScrapInfo{e, m_admin.get(e).value}); + result.push_back(ScrapInfo{e, m_admin.get(e).value, sd.amount}); }); return result; } diff --git a/src/lib/ecs/system/ScrapSystem.h b/src/lib/ecs/system/ScrapSystem.h index da91236..4f75b9e 100644 --- a/src/lib/ecs/system/ScrapSystem.h +++ b/src/lib/ecs/system/ScrapSystem.h @@ -15,6 +15,7 @@ struct ScrapInfo { entt::entity entity; QVector2D position; + int amount; }; class ScrapSystem diff --git a/src/lib/eventsystem/event/ScrapSelectionChangedEvent.h b/src/lib/eventsystem/event/ScrapSelectionChangedEvent.h new file mode 100644 index 0000000..36bc7fd --- /dev/null +++ b/src/lib/eventsystem/event/ScrapSelectionChangedEvent.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include "entt/entity/entity.hpp" + +#include "Event.h" + +// The set of currently selected scrap piles (REQ-UI-SCRAP-CLICK-SELECT, +// REQ-UI-SCRAP-MULTI-SELECT). An empty list means no scrap is selected. Scrap forms +// its own selection category, mutually exclusive with buildings and entities. +class ScrapSelectionChangedEvent : public Event +{ +public: + explicit ScrapSelectionChangedEvent(std::vector scrap) + : scrap(std::move(scrap)) {} + const std::vector scrap; +}; diff --git a/src/lib/sim/EntityHitTest.cpp b/src/lib/sim/EntityHitTest.cpp index da84e28..948dbdf 100644 --- a/src/lib/sim/EntityHitTest.cpp +++ b/src/lib/sim/EntityHitTest.cpp @@ -1,9 +1,11 @@ #include "EntityHitTest.h" +#include #include #include "EntityAdmin.h" #include "PositionComponent.h" +#include "ScrapDataComponent.h" #include "StationBodyComponent.h" #include "HealthComponent.h" @@ -54,3 +56,48 @@ entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos) return bestShip; } + +entt::entity scrapAtWorldPos(EntityAdmin& admin, QVector2D worldPos) +{ + // Slightly larger than the scrap's rendered radius (0.2 tiles) so small piles + // remain easy to click; tunable. + constexpr float kScrapHitRadiusSquared = 0.35f * 0.35f; + entt::entity bestScrap = entt::null; + float bestDistSquared = kScrapHitRadiusSquared; + + admin.forEach( + [&](entt::entity entity, const ScrapDataComponent& /*sd*/, const PositionComponent& pos) + { + const float dx = pos.value.x() - worldPos.x(); + const float dy = pos.value.y() - worldPos.y(); + const float distSquared = dx * dx + dy * dy; + if (distSquared < bestDistSquared) + { + bestDistSquared = distSquared; + bestScrap = entity; + } + }); + + return bestScrap; +} + +std::vector scrapInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB) +{ + const int minX = std::min(tileA.x(), tileB.x()); + const int maxX = std::max(tileA.x(), tileB.x()); + const int minY = std::min(tileA.y(), tileB.y()); + const int maxY = std::max(tileA.y(), tileB.y()); + + std::vector result; + admin.forEach( + [&](entt::entity entity, const ScrapDataComponent& /*sd*/, const PositionComponent& pos) + { + const int tileX = static_cast(std::floor(pos.value.x())); + const int tileY = static_cast(std::floor(pos.value.y())); + if (tileX >= minX && tileX <= maxX && tileY >= minY && tileY <= maxY) + { + result.push_back(entity); + } + }); + return result; +} diff --git a/src/lib/sim/EntityHitTest.h b/src/lib/sim/EntityHitTest.h index 716611d..5ca4f3f 100644 --- a/src/lib/sim/EntityHitTest.h +++ b/src/lib/sim/EntityHitTest.h @@ -1,5 +1,8 @@ #pragma once +#include + +#include #include #include "entt/entity/entity.hpp" @@ -7,3 +10,12 @@ class EntityAdmin; entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos); + +// Returns the nearest scrap pile whose center is within the scrap pick radius of +// worldPos, or entt::null if none (REQ-UI-SCRAP-CLICK-SELECT). Scrap is picked only +// after actors: entityAtWorldPos never returns scrap (scrap has no HealthComponent). +entt::entity scrapAtWorldPos(EntityAdmin& admin, QVector2D worldPos); + +// Returns every scrap pile whose position falls within the inclusive tile rectangle +// spanned by tileA and tileB, in any corner order (REQ-UI-SCRAP-MULTI-SELECT). +std::vector scrapInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB); diff --git a/src/test/ScrapTest.cpp b/src/test/ScrapTest.cpp index fce5d47..dcc4f14 100644 --- a/src/test/ScrapTest.cpp +++ b/src/test/ScrapTest.cpp @@ -2,11 +2,22 @@ #include +#include + #include "DespawnAtComponent.h" #include "EntityAdmin.h" +#include "EntityHitTest.h" #include "ScrapDataComponent.h" #include "ScrapSystem.h" +namespace +{ +bool contains(const std::vector& v, entt::entity e) +{ + return std::find(v.begin(), v.end(), e) != v.end(); +} +} // namespace + // --------------------------------------------------------------------------- // Spawn // --------------------------------------------------------------------------- @@ -140,3 +151,78 @@ TEST_CASE("ScrapSystem: allScrapInfo returns all spawned scrap", "[scrap]") const std::vector info = ss.allScrapInfo(); REQUIRE(info.size() == 2); } + +TEST_CASE("ScrapSystem: allScrapInfo reports each pile's remaining amount", "[scrap]") +{ + EntityAdmin admin; + ScrapSystem ss(admin); + + const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100); + const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200); + + const std::vector info = ss.allScrapInfo(); + REQUIRE(info.size() == 2); + for (const ScrapInfo& i : info) + { + if (i.entity == a) { REQUIRE(i.amount == 3); } + else if (i.entity == b) { REQUIRE(i.amount == 6); } + else { FAIL("unexpected scrap entity"); } + } +} + +// --------------------------------------------------------------------------- +// Selection hit-testing (REQ-UI-SCRAP-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT) +// --------------------------------------------------------------------------- + +TEST_CASE("scrapAtWorldPos returns the pile near a point and null when far", "[scrap]") +{ + EntityAdmin admin; + ScrapSystem ss(admin); + + const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100); + + // Extra parens keep Catch from decomposing the comparison, which is ambiguous + // between Catch's expression templates and entt's entity operator==. + REQUIRE((scrapAtWorldPos(admin, QVector2D(3.1f, 4.0f)) == e)); + REQUIRE((scrapAtWorldPos(admin, QVector2D(10.0f, 10.0f)) == entt::null)); +} + +TEST_CASE("scrapAtWorldPos returns the nearest of several piles", "[scrap]") +{ + EntityAdmin admin; + ScrapSystem ss(admin); + + const entt::entity near = ss.spawn(QVector2D(2.0f, 2.0f), 1, 100); + ss.spawn(QVector2D(2.4f, 2.0f), 1, 100); + + REQUIRE((scrapAtWorldPos(admin, QVector2D(2.05f, 2.0f)) == near)); +} + +TEST_CASE("entityAtWorldPos never returns a scrap pile", "[scrap]") +{ + EntityAdmin admin; + ScrapSystem ss(admin); + + ss.spawn(QVector2D(3.0f, 4.0f), 5, 100); + + // Scrap has no HealthComponent, so the actor hit-test ignores it entirely. + REQUIRE((entityAtWorldPos(admin, QVector2D(3.0f, 4.0f)) == entt::null)); +} + +TEST_CASE("scrapInBox returns exactly the piles inside the tile rectangle", "[scrap]") +{ + EntityAdmin admin; + ScrapSystem ss(admin); + + const entt::entity inA = ss.spawn(QVector2D(1.2f, 2.7f), 1, 100); // tile (1,2) + const entt::entity inB = ss.spawn(QVector2D(4.9f, 5.1f), 1, 100); // tile (4,5) + const entt::entity outX = ss.spawn(QVector2D(10.0f, 10.0f), 1, 100); + + // Box given in reversed corner order to confirm normalization. + const std::vector hit = scrapInBox(admin, QPoint(5, 5), QPoint(0, 0)); + + REQUIRE(hit.size() == 2); + REQUIRE(contains(hit, inA)); + REQUIRE(contains(hit, inB)); + REQUIRE_FALSE(contains(hit, outX)); +} diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index c424eeb..59525d6 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -43,6 +43,7 @@ #include "PositionComponent.h" #include "RepairBehavior.h" #include "SalvageScrapBehavior.h" +#include "ScrapSelectionChangedEvent.h" #include "ScrapSystem.h" #include "SelectionChangedEvent.h" #include "SensorRangeComponent.h" @@ -271,6 +272,10 @@ void GameWorldView::onFrame() m_activeBeams = std::move(live); } + // Drop selected scrap piles that were collected or despawned this frame, so the + // panel stops counting them and the selection empties out (REQ-UI-SCRAP-CLICK-SELECT). + pruneDespawnedScrap(); + // Expire copy/paste flashes. Lifetime is wall-clock (the frame delta), so the // flash plays for a fixed real duration regardless of game speed, including // while the game is paused (REQ-BLD-COPY-CONFIG-FEEDBACK). @@ -666,6 +671,36 @@ std::optional GameWorldView::entityPosition(entt::entity entity) cons return m_sim->admin().get(entity).value; } +void GameWorldView::clearScrapSelection() +{ + if (m_selectedScrap.empty()) { return; } + m_selectedScrap.clear(); + EventManager::getInstance()->sendEventImmediately( + std::make_shared(m_selectedScrap)); +} + +void GameWorldView::pruneDespawnedScrap() +{ + if (m_selectedScrap.empty()) { return; } + + std::vector live; + for (const ScrapInfo& info : m_sim->scraps().allScrapInfo()) + { + if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity) + != m_selectedScrap.end()) + { + live.push_back(info.entity); + } + } + + if (live.size() != m_selectedScrap.size()) + { + m_selectedScrap = std::move(live); + EventManager::getInstance()->sendEventImmediately( + std::make_shared(m_selectedScrap)); + } +} + void GameWorldView::stepSpeed(int delta) { const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 }; @@ -1752,6 +1787,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) if (hitEntity != entt::null) { + // Actors (ship/station) win over scrap and buildings (REQ-UI-SCRAP-CLICK-SELECT). + clearScrapSelection(); m_selectedBuildingIds.clear(); EventManager::getInstance()->sendEventImmediately( std::make_shared(m_selectedBuildingIds)); @@ -1775,6 +1812,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) } if (id != kInvalidBuildingId) { + // A building/construction site outranks scrap (REQ-UI-SCRAP-CLICK-SELECT). + clearScrapSelection(); if (event->modifiers() & Qt::ControlModifier) { bool found = false; @@ -1794,6 +1833,36 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) EventManager::getInstance()->sendEventImmediately( std::make_shared(m_selectedBuildingIds)); } + else if (const entt::entity scrapHit = + scrapAtWorldPos(m_sim->admin(), worldPos); scrapHit != entt::null) + { + // Scrap forms its own selection category; picking it clears any + // building selection (REQ-UI-SCRAP-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT). + if (!m_selectedBuildingIds.empty()) + { + m_selectedBuildingIds.clear(); + EventManager::getInstance()->sendEventImmediately( + std::make_shared(m_selectedBuildingIds)); + } + if (event->modifiers() & Qt::ControlModifier) + { + bool found = false; + std::vector newSel; + for (entt::entity sel : m_selectedScrap) + { + if (sel == scrapHit) { found = true; } + else { newSel.push_back(sel); } + } + if (!found) { newSel.push_back(scrapHit); } + m_selectedScrap = newSel; + } + else + { + m_selectedScrap = { scrapHit }; + } + EventManager::getInstance()->sendEventImmediately( + std::make_shared(m_selectedScrap)); + } else { if (!(event->modifiers() & Qt::ControlModifier)) @@ -1801,6 +1870,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) m_selectedBuildingIds.clear(); EventManager::getInstance()->sendEventImmediately( std::make_shared(m_selectedBuildingIds)); + clearScrapSelection(); } m_boxSelecting = true; m_boxStartTile = tile; @@ -1873,24 +1943,75 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event) return; } - if (!(event->modifiers() & Qt::ControlModifier)) + const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0; + + if (!boxIds.empty()) { - m_selectedBuildingIds = boxIds; - } - else - { - for (BuildingId id : boxIds) + // A box covering any building selects buildings; scrap in the box is + // ignored (REQ-UI-MULTI-SELECT, REQ-UI-SCRAP-MULTI-SELECT). + clearScrapSelection(); + if (!ctrl) { - bool found = false; - for (BuildingId sel : m_selectedBuildingIds) - { - if (sel == id) { found = true; break; } - } - if (!found) { m_selectedBuildingIds.push_back(id); } + m_selectedBuildingIds = boxIds; } + else + { + for (BuildingId id : boxIds) + { + bool found = false; + for (BuildingId sel : m_selectedBuildingIds) + { + if (sel == id) { found = true; break; } + } + if (!found) { m_selectedBuildingIds.push_back(id); } + } + } + EventManager::getInstance()->sendEventImmediately( + std::make_shared(m_selectedBuildingIds)); + return; + } + + // No buildings in the box: a scrap-only box selects the scrap it covers + // (REQ-UI-SCRAP-MULTI-SELECT). + const std::vector boxScrap = + scrapInBox(m_sim->admin(), m_boxStartTile, m_boxCurrentTile); + if (!boxScrap.empty()) + { + if (!m_selectedBuildingIds.empty()) + { + m_selectedBuildingIds.clear(); + EventManager::getInstance()->sendEventImmediately( + std::make_shared(m_selectedBuildingIds)); + } + if (!ctrl) + { + m_selectedScrap = boxScrap; + } + else + { + for (entt::entity e : boxScrap) + { + bool found = false; + for (entt::entity sel : m_selectedScrap) + { + if (sel == e) { found = true; break; } + } + if (!found) { m_selectedScrap.push_back(e); } + } + } + EventManager::getInstance()->sendEventImmediately( + std::make_shared(m_selectedScrap)); + return; + } + + // Empty box: a plain (non-additive) drag clears the current selection. + if (!ctrl) + { + m_selectedBuildingIds.clear(); + EventManager::getInstance()->sendEventImmediately( + std::make_shared(m_selectedBuildingIds)); + clearScrapSelection(); } - EventManager::getInstance()->sendEventImmediately( - std::make_shared(m_selectedBuildingIds)); } } diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index 5e84f04..ead933c 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -168,6 +168,13 @@ private: void placeBlueprintAtTile(QPoint center); std::optional entityPosition(entt::entity entity) const; + // Clears the scrap selection, emitting an empty ScrapSelectionChangedEvent when + // it was non-empty (REQ-UI-SCRAP-CLICK-SELECT). Used when another selection + // category takes over. + void clearScrapSelection(); + // Drops despawned or fully-collected piles from the scrap selection and re-emits + // when it changed (REQ-UI-SCRAP-CLICK-SELECT). Called each frame from onFrame(). + void pruneDespawnedScrap(); void stepSpeed(int delta); void placeAtTile(QPoint tile); @@ -249,6 +256,7 @@ private: std::vector m_selectedBuildingIds; std::optional m_selectedEntity; + std::vector m_selectedScrap; bool m_boxSelecting; QPoint m_boxStartTile; QPoint m_boxCurrentTile; diff --git a/src/ui/SelectedBuildingPanel.cpp b/src/ui/SelectedBuildingPanel.cpp index 926b8f8..1d6ca72 100644 --- a/src/ui/SelectedBuildingPanel.cpp +++ b/src/ui/SelectedBuildingPanel.cpp @@ -37,6 +37,7 @@ #include "RecipeSelectionDialog.h" #include "RecipeSelectionRequestedEvent.h" #include "Rotation.h" +#include "ScrapSystem.h" #include "ShipLayoutPreview.h" #include "Simulation.h" #include "WeaponComponent.h" @@ -190,6 +191,10 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim, m_layout->addWidget(m_stationStatsLabel); m_stationStatsLabel->hide(); + m_scrapLabel = new QLabel(this); + m_layout->addWidget(m_scrapLabel); + m_scrapLabel->hide(); + buildEmpty(); registerForEvents(); @@ -206,6 +211,9 @@ void SelectedBuildingPanel::onSelectionChanged(const std::vector& id if (!ids.empty()) { clearEntityDisplay(); + // A building selection supersedes any scrap selection (REQ-UI-SCRAP-CLICK-SELECT). + m_selectedScrap.clear(); + m_scrapLabel->hide(); } rebuild(); } @@ -238,6 +246,7 @@ void SelectedBuildingPanel::hideAllWidgets() m_filterBLabel->hide(); m_filterBList->hide(); m_buffersLabel->hide(); + m_scrapLabel->hide(); } void SelectedBuildingPanel::clearContent() @@ -638,6 +647,13 @@ void SelectedBuildingPanel::handleEvent( void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason) { + if (!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(); @@ -885,6 +901,9 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptrentity; m_selectedBuildingIds.clear(); + // An entity selection supersedes any scrap selection (REQ-UI-SCRAP-CLICK-SELECT). + m_selectedScrap.clear(); + m_scrapLabel->hide(); clearContent(); EntityAdmin& admin = m_sim->admin(); @@ -1022,6 +1041,54 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptrids); } +void SelectedBuildingPanel::handleEvent( + std::shared_ptr event) +{ + 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). + m_selectedBuildingIds.clear(); + clearContent(); + clearEntityDisplay(); + buildScrap(); + } + else + { + m_scrapLabel->hide(); + if (m_selectedBuildingIds.empty() && !m_selectedEntity.has_value()) + { + buildEmpty(); + } + } +} + +void SelectedBuildingPanel::buildScrap() +{ + clearContent(); + m_entityTitleLabel->hide(); + m_entityStatsPanel->hide(); + m_stationStatsLabel->hide(); + refreshScrapTotal(); + m_scrapLabel->show(); +} + +void SelectedBuildingPanel::refreshScrapTotal() +{ + // Sum the remaining amounts of the still-living selected piles (REQ-UI-SCRAP-PANEL). + int total = 0; + for (const ScrapInfo& info : m_sim->scraps().allScrapInfo()) + { + if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity) + != m_selectedScrap.end()) + { + total += info.amount; + } + } + m_scrapLabel->setText(tr("Scrap: %1").arg(total)); +} + void SelectedBuildingPanel::handleEvent(std::shared_ptr event) { m_debugDraw = event->active; diff --git a/src/ui/SelectedBuildingPanel.h b/src/ui/SelectedBuildingPanel.h index 1aac58a..342da51 100644 --- a/src/ui/SelectedBuildingPanel.h +++ b/src/ui/SelectedBuildingPanel.h @@ -18,6 +18,7 @@ #include "GameConfig.h" #include "PlayerCommandsAppliedEvent.h" #include "RecipesConfig.h" +#include "ScrapSelectionChangedEvent.h" #include "SelectionChangedEvent.h" #include "ShipLayout.h" #include "ShipsConfig.h" @@ -37,6 +38,7 @@ class SelectedBuildingPanel : public QWidget, PlayerCommandsAppliedEvent, EntitySelectedEvent, SelectionChangedEvent, + ScrapSelectionChangedEvent, DebugDrawToggledEvent> { Q_OBJECT @@ -51,6 +53,7 @@ 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; void handleEvent(std::shared_ptr event) override; private slots: @@ -77,6 +80,8 @@ private: void buildEmpty(); void buildSingle(BuildingId id); void buildMulti(const std::vector& ids); + void buildScrap(); + void refreshScrapTotal(); void refreshBuffers(const Building* b); void refreshSiteProgress(const ConstructionSite* s); void updateShipyardLayoutWidgets(BuildingType type, @@ -115,6 +120,9 @@ private: QLabel* m_entityTitleLabel; QLabel* m_stationStatsLabel; + std::vector m_selectedScrap; + QLabel* m_scrapLabel; + void buildEntityShip(entt::entity entity); void buildEntityStation(entt::entity entity); void refreshEntityStats();