allow to (multi) select scrap

This commit is contained in:
2026-07-13 21:09:01 +02:00
parent 8c4fb78fc9
commit 535d4f8f24
11 changed files with 403 additions and 19 deletions

View File

@@ -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 };
@@ -1048,6 +1083,19 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
// Outline sits 1px outside the footprint (into adjacent tiles).
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
}
// A ring around each selected scrap pile, sitting just outside the pile's
// rendered circle (radius tilePx()*0.2, matching drawScrap) (REQ-UI-SCRAP-CLICK-SELECT).
if (!m_selectedScrap.empty())
{
const qreal outlineRadius = static_cast<qreal>(tilePx() * 0.2f) + 3.0;
for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo())
{
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), scrap.entity)
== m_selectedScrap.end()) { continue; }
painter.drawEllipse(worldToWidget(scrap.position), outlineRadius, outlineRadius);
}
}
}
void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
@@ -1752,6 +1800,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 +1825,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 +1846,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 +1883,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 +1956,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));
}
}