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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DZR44tA8sn4dPqDzAVXyps
This commit is contained in:
@@ -69,9 +69,9 @@ std::vector<ScrapInfo> ScrapSystem::allScrapInfo() const
|
||||
{
|
||||
std::vector<ScrapInfo> result;
|
||||
m_admin.forEach<ScrapDataComponent>(
|
||||
[&result, this](entt::entity e, const ScrapDataComponent& /*sd*/)
|
||||
[&result, this](entt::entity e, const ScrapDataComponent& sd)
|
||||
{
|
||||
result.push_back(ScrapInfo{e, m_admin.get<PositionComponent>(e).value});
|
||||
result.push_back(ScrapInfo{e, m_admin.get<PositionComponent>(e).value, sd.amount});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct ScrapInfo
|
||||
{
|
||||
entt::entity entity;
|
||||
QVector2D position;
|
||||
int amount;
|
||||
};
|
||||
|
||||
class ScrapSystem
|
||||
|
||||
18
src/lib/eventsystem/event/ScrapSelectionChangedEvent.h
Normal file
18
src/lib/eventsystem/event/ScrapSelectionChangedEvent.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#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<entt::entity> scrap)
|
||||
: scrap(std::move(scrap)) {}
|
||||
const std::vector<entt::entity> scrap;
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "EntityHitTest.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#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<ScrapDataComponent, PositionComponent>(
|
||||
[&](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<entt::entity> 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<entt::entity> result;
|
||||
admin.forEach<ScrapDataComponent, PositionComponent>(
|
||||
[&](entt::entity entity, const ScrapDataComponent& /*sd*/, const PositionComponent& pos)
|
||||
{
|
||||
const int tileX = static_cast<int>(std::floor(pos.value.x()));
|
||||
const int tileY = static_cast<int>(std::floor(pos.value.y()));
|
||||
if (tileX >= minX && tileX <= maxX && tileY >= minY && tileY <= maxY)
|
||||
{
|
||||
result.push_back(entity);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
#include <QVector2D>
|
||||
|
||||
#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<entt::entity> scrapInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB);
|
||||
|
||||
@@ -2,11 +2,22 @@
|
||||
|
||||
#include <QVector2D>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "DespawnAtComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "EntityHitTest.h"
|
||||
#include "ScrapDataComponent.h"
|
||||
#include "ScrapSystem.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
bool contains(const std::vector<entt::entity>& 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<ScrapInfo> 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<ScrapInfo> 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<entt::entity> 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));
|
||||
}
|
||||
|
||||
@@ -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<QVector2D> GameWorldView::entityPosition(entt::entity entity) cons
|
||||
return m_sim->admin().get<PositionComponent>(entity).value;
|
||||
}
|
||||
|
||||
void GameWorldView::clearScrapSelection()
|
||||
{
|
||||
if (m_selectedScrap.empty()) { return; }
|
||||
m_selectedScrap.clear();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap));
|
||||
}
|
||||
|
||||
void GameWorldView::pruneDespawnedScrap()
|
||||
{
|
||||
if (m_selectedScrap.empty()) { return; }
|
||||
|
||||
std::vector<entt::entity> 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<ScrapSelectionChangedEvent>(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<SelectionChangedEvent>(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<SelectionChangedEvent>(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<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
}
|
||||
if (event->modifiers() & Qt::ControlModifier)
|
||||
{
|
||||
bool found = false;
|
||||
std::vector<entt::entity> 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<ScrapSelectionChangedEvent>(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<SelectionChangedEvent>(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<SelectionChangedEvent>(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<entt::entity> 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<SelectionChangedEvent>(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<ScrapSelectionChangedEvent>(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<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
clearScrapSelection();
|
||||
}
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,13 @@ private:
|
||||
void placeBlueprintAtTile(QPoint center);
|
||||
|
||||
std::optional<QVector2D> 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<BuildingId> m_selectedBuildingIds;
|
||||
std::optional<entt::entity> m_selectedEntity;
|
||||
std::vector<entt::entity> m_selectedScrap;
|
||||
bool m_boxSelecting;
|
||||
QPoint m_boxStartTile;
|
||||
QPoint m_boxCurrentTile;
|
||||
|
||||
@@ -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<BuildingId>& 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_ptr<const EntitySelectedEven
|
||||
{
|
||||
m_selectedEntity = event->entity;
|
||||
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_ptr<const SelectionChangedEv
|
||||
onSelectionChanged(event->ids);
|
||||
}
|
||||
|
||||
void SelectedBuildingPanel::handleEvent(
|
||||
std::shared_ptr<const ScrapSelectionChangedEvent> 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<const DebugDrawToggledEvent> event)
|
||||
{
|
||||
m_debugDraw = event->active;
|
||||
|
||||
@@ -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<const PlayerCommandsAppliedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const ScrapSelectionChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
||||
|
||||
private slots:
|
||||
@@ -77,6 +80,8 @@ private:
|
||||
void buildEmpty();
|
||||
void buildSingle(BuildingId id);
|
||||
void buildMulti(const std::vector<BuildingId>& 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<entt::entity> m_selectedScrap;
|
||||
QLabel* m_scrapLabel;
|
||||
|
||||
void buildEntityShip(entt::entity entity);
|
||||
void buildEntityStation(entt::entity entity);
|
||||
void refreshEntityStats();
|
||||
|
||||
Reference in New Issue
Block a user