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;
|
std::vector<ScrapInfo> result;
|
||||||
m_admin.forEach<ScrapDataComponent>(
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct ScrapInfo
|
|||||||
{
|
{
|
||||||
entt::entity entity;
|
entt::entity entity;
|
||||||
QVector2D position;
|
QVector2D position;
|
||||||
|
int amount;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ScrapSystem
|
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 "EntityHitTest.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
|
#include "ScrapDataComponent.h"
|
||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
|
|
||||||
@@ -54,3 +56,48 @@ entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos)
|
|||||||
|
|
||||||
return bestShip;
|
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
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
@@ -7,3 +10,12 @@
|
|||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
|
|
||||||
entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
|
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 <QVector2D>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "DespawnAtComponent.h"
|
#include "DespawnAtComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
|
#include "EntityHitTest.h"
|
||||||
#include "ScrapDataComponent.h"
|
#include "ScrapDataComponent.h"
|
||||||
#include "ScrapSystem.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
|
// Spawn
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -140,3 +151,78 @@ TEST_CASE("ScrapSystem: allScrapInfo returns all spawned scrap", "[scrap]")
|
|||||||
const std::vector<ScrapInfo> info = ss.allScrapInfo();
|
const std::vector<ScrapInfo> info = ss.allScrapInfo();
|
||||||
REQUIRE(info.size() == 2);
|
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 "PositionComponent.h"
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehavior.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
#include "SalvageScrapBehavior.h"
|
||||||
|
#include "ScrapSelectionChangedEvent.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
@@ -271,6 +272,10 @@ void GameWorldView::onFrame()
|
|||||||
m_activeBeams = std::move(live);
|
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
|
// 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
|
// flash plays for a fixed real duration regardless of game speed, including
|
||||||
// while the game is paused (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
// 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;
|
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)
|
void GameWorldView::stepSpeed(int delta)
|
||||||
{
|
{
|
||||||
const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
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)
|
if (hitEntity != entt::null)
|
||||||
{
|
{
|
||||||
|
// Actors (ship/station) win over scrap and buildings (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
clearScrapSelection();
|
||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
@@ -1775,6 +1812,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
}
|
}
|
||||||
if (id != kInvalidBuildingId)
|
if (id != kInvalidBuildingId)
|
||||||
{
|
{
|
||||||
|
// A building/construction site outranks scrap (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
clearScrapSelection();
|
||||||
if (event->modifiers() & Qt::ControlModifier)
|
if (event->modifiers() & Qt::ControlModifier)
|
||||||
{
|
{
|
||||||
bool found = false;
|
bool found = false;
|
||||||
@@ -1794,6 +1833,36 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
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
|
else
|
||||||
{
|
{
|
||||||
if (!(event->modifiers() & Qt::ControlModifier))
|
if (!(event->modifiers() & Qt::ControlModifier))
|
||||||
@@ -1801,6 +1870,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
|
clearScrapSelection();
|
||||||
}
|
}
|
||||||
m_boxSelecting = true;
|
m_boxSelecting = true;
|
||||||
m_boxStartTile = tile;
|
m_boxStartTile = tile;
|
||||||
@@ -1873,24 +1943,75 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(event->modifiers() & Qt::ControlModifier))
|
const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0;
|
||||||
|
|
||||||
|
if (!boxIds.empty())
|
||||||
{
|
{
|
||||||
m_selectedBuildingIds = boxIds;
|
// A box covering any building selects buildings; scrap in the box is
|
||||||
}
|
// ignored (REQ-UI-MULTI-SELECT, REQ-UI-SCRAP-MULTI-SELECT).
|
||||||
else
|
clearScrapSelection();
|
||||||
{
|
if (!ctrl)
|
||||||
for (BuildingId id : boxIds)
|
|
||||||
{
|
{
|
||||||
bool found = false;
|
m_selectedBuildingIds = boxIds;
|
||||||
for (BuildingId sel : m_selectedBuildingIds)
|
|
||||||
{
|
|
||||||
if (sel == id) { found = true; break; }
|
|
||||||
}
|
|
||||||
if (!found) { m_selectedBuildingIds.push_back(id); }
|
|
||||||
}
|
}
|
||||||
|
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);
|
void placeBlueprintAtTile(QPoint center);
|
||||||
|
|
||||||
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
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 stepSpeed(int delta);
|
||||||
void placeAtTile(QPoint tile);
|
void placeAtTile(QPoint tile);
|
||||||
|
|
||||||
@@ -249,6 +256,7 @@ private:
|
|||||||
|
|
||||||
std::vector<BuildingId> m_selectedBuildingIds;
|
std::vector<BuildingId> m_selectedBuildingIds;
|
||||||
std::optional<entt::entity> m_selectedEntity;
|
std::optional<entt::entity> m_selectedEntity;
|
||||||
|
std::vector<entt::entity> m_selectedScrap;
|
||||||
bool m_boxSelecting;
|
bool m_boxSelecting;
|
||||||
QPoint m_boxStartTile;
|
QPoint m_boxStartTile;
|
||||||
QPoint m_boxCurrentTile;
|
QPoint m_boxCurrentTile;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#include "RecipeSelectionDialog.h"
|
#include "RecipeSelectionDialog.h"
|
||||||
#include "RecipeSelectionRequestedEvent.h"
|
#include "RecipeSelectionRequestedEvent.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "ScrapSystem.h"
|
||||||
#include "ShipLayoutPreview.h"
|
#include "ShipLayoutPreview.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "WeaponComponent.h"
|
#include "WeaponComponent.h"
|
||||||
@@ -190,6 +191,10 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
|||||||
m_layout->addWidget(m_stationStatsLabel);
|
m_layout->addWidget(m_stationStatsLabel);
|
||||||
m_stationStatsLabel->hide();
|
m_stationStatsLabel->hide();
|
||||||
|
|
||||||
|
m_scrapLabel = new QLabel(this);
|
||||||
|
m_layout->addWidget(m_scrapLabel);
|
||||||
|
m_scrapLabel->hide();
|
||||||
|
|
||||||
buildEmpty();
|
buildEmpty();
|
||||||
|
|
||||||
registerForEvents();
|
registerForEvents();
|
||||||
@@ -206,6 +211,9 @@ void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& id
|
|||||||
if (!ids.empty())
|
if (!ids.empty())
|
||||||
{
|
{
|
||||||
clearEntityDisplay();
|
clearEntityDisplay();
|
||||||
|
// A building selection supersedes any scrap selection (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
m_selectedScrap.clear();
|
||||||
|
m_scrapLabel->hide();
|
||||||
}
|
}
|
||||||
rebuild();
|
rebuild();
|
||||||
}
|
}
|
||||||
@@ -238,6 +246,7 @@ void SelectedBuildingPanel::hideAllWidgets()
|
|||||||
m_filterBLabel->hide();
|
m_filterBLabel->hide();
|
||||||
m_filterBList->hide();
|
m_filterBList->hide();
|
||||||
m_buffersLabel->hide();
|
m_buffersLabel->hide();
|
||||||
|
m_scrapLabel->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectedBuildingPanel::clearContent()
|
void SelectedBuildingPanel::clearContent()
|
||||||
@@ -638,6 +647,13 @@ void SelectedBuildingPanel::handleEvent(
|
|||||||
|
|
||||||
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
|
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())
|
if (m_selectedEntity.has_value())
|
||||||
{
|
{
|
||||||
refreshEntityStats();
|
refreshEntityStats();
|
||||||
@@ -885,6 +901,9 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectedEven
|
|||||||
{
|
{
|
||||||
m_selectedEntity = event->entity;
|
m_selectedEntity = event->entity;
|
||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
|
// An entity selection supersedes any scrap selection (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
m_selectedScrap.clear();
|
||||||
|
m_scrapLabel->hide();
|
||||||
clearContent();
|
clearContent();
|
||||||
|
|
||||||
EntityAdmin& admin = m_sim->admin();
|
EntityAdmin& admin = m_sim->admin();
|
||||||
@@ -1022,6 +1041,54 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEv
|
|||||||
onSelectionChanged(event->ids);
|
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)
|
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)
|
||||||
{
|
{
|
||||||
m_debugDraw = event->active;
|
m_debugDraw = event->active;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "PlayerCommandsAppliedEvent.h"
|
#include "PlayerCommandsAppliedEvent.h"
|
||||||
#include "RecipesConfig.h"
|
#include "RecipesConfig.h"
|
||||||
|
#include "ScrapSelectionChangedEvent.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
@@ -37,6 +38,7 @@ class SelectedBuildingPanel : public QWidget,
|
|||||||
PlayerCommandsAppliedEvent,
|
PlayerCommandsAppliedEvent,
|
||||||
EntitySelectedEvent,
|
EntitySelectedEvent,
|
||||||
SelectionChangedEvent,
|
SelectionChangedEvent,
|
||||||
|
ScrapSelectionChangedEvent,
|
||||||
DebugDrawToggledEvent>
|
DebugDrawToggledEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -51,6 +53,7 @@ private:
|
|||||||
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
|
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectedEvent> 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 SelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const ScrapSelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
@@ -77,6 +80,8 @@ private:
|
|||||||
void buildEmpty();
|
void buildEmpty();
|
||||||
void buildSingle(BuildingId id);
|
void buildSingle(BuildingId id);
|
||||||
void buildMulti(const std::vector<BuildingId>& ids);
|
void buildMulti(const std::vector<BuildingId>& ids);
|
||||||
|
void buildScrap();
|
||||||
|
void refreshScrapTotal();
|
||||||
void refreshBuffers(const Building* b);
|
void refreshBuffers(const Building* b);
|
||||||
void refreshSiteProgress(const ConstructionSite* s);
|
void refreshSiteProgress(const ConstructionSite* s);
|
||||||
void updateShipyardLayoutWidgets(BuildingType type,
|
void updateShipyardLayoutWidgets(BuildingType type,
|
||||||
@@ -115,6 +120,9 @@ private:
|
|||||||
QLabel* m_entityTitleLabel;
|
QLabel* m_entityTitleLabel;
|
||||||
QLabel* m_stationStatsLabel;
|
QLabel* m_stationStatsLabel;
|
||||||
|
|
||||||
|
std::vector<entt::entity> m_selectedScrap;
|
||||||
|
QLabel* m_scrapLabel;
|
||||||
|
|
||||||
void buildEntityShip(entt::entity entity);
|
void buildEntityShip(entt::entity entity);
|
||||||
void buildEntityStation(entt::entity entity);
|
void buildEntityStation(entt::entity entity);
|
||||||
void refreshEntityStats();
|
void refreshEntityStats();
|
||||||
|
|||||||
Reference in New Issue
Block a user