allow multi-select for ships/stations, mixable with scrap
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
#include "ReplayRecorder.h"
|
||||
#include "DemolishModeChangedEvent.h"
|
||||
#include "EntityHitTest.h"
|
||||
#include "EntitySelectedEvent.h"
|
||||
#include "EntitySelectionChangedEvent.h"
|
||||
#include "EventManager.h"
|
||||
#include "FacingComponent.h"
|
||||
#include "FactionComponent.h"
|
||||
@@ -284,6 +284,7 @@ void GameWorldView::onFrame()
|
||||
// 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();
|
||||
pruneDespawnedActors();
|
||||
|
||||
// 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
|
||||
@@ -725,6 +726,43 @@ void GameWorldView::pruneDespawnedScrap()
|
||||
}
|
||||
}
|
||||
|
||||
void GameWorldView::clearEntitySelection()
|
||||
{
|
||||
if (m_selectedEntities.empty()) { return; }
|
||||
m_selectedEntities.clear();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectionChangedEvent>(m_selectedEntities));
|
||||
}
|
||||
|
||||
void GameWorldView::pruneDespawnedActors()
|
||||
{
|
||||
if (m_selectedEntities.empty()) { return; }
|
||||
|
||||
EntityAdmin& admin = m_sim->getAdmin();
|
||||
std::vector<entt::entity> live;
|
||||
for (entt::entity e : m_selectedEntities)
|
||||
{
|
||||
if (admin.isValid(e) && admin.hasAll<HealthComponent>(e)
|
||||
&& admin.get<HealthComponent>(e).hp > 0.0f)
|
||||
{
|
||||
live.push_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (live.size() != m_selectedEntities.size())
|
||||
{
|
||||
m_selectedEntities = std::move(live);
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectionChangedEvent>(m_selectedEntities));
|
||||
}
|
||||
}
|
||||
|
||||
bool GameWorldView::isEntitySelected(entt::entity entity) const
|
||||
{
|
||||
return std::find(m_selectedEntities.begin(), m_selectedEntities.end(), entity)
|
||||
!= m_selectedEntities.end();
|
||||
}
|
||||
|
||||
void GameWorldView::stepSpeed(int delta)
|
||||
{
|
||||
const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
||||
@@ -1305,7 +1343,7 @@ void GameWorldView::drawStations(QPainter& painter)
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(bboxRect);
|
||||
|
||||
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
|
||||
if (isEntitySelected(e))
|
||||
{
|
||||
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
@@ -1352,7 +1390,7 @@ void GameWorldView::drawShips(QPainter& painter)
|
||||
painter.setBrush(it->second.fill);
|
||||
painter.drawPolygon(tri);
|
||||
|
||||
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
|
||||
if (isEntitySelected(e))
|
||||
{
|
||||
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
@@ -2040,102 +2078,128 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
}
|
||||
}
|
||||
|
||||
const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0;
|
||||
const QVector2D worldPos = widgetToWorld(event->pos());
|
||||
const entt::entity hitEntity = entityAtWorldPos(m_sim->getAdmin(), worldPos);
|
||||
|
||||
if (hitEntity != entt::null)
|
||||
// Point hit-test precedence: buildings win over actors, which win over scrap
|
||||
// (REQ-UI-SELECTION-CATEGORIES).
|
||||
std::optional<BuildingId> buildingHit = buildingAtTile(tile);
|
||||
if (!buildingHit.has_value()) { buildingHit = siteAtTile(tile); }
|
||||
|
||||
if (buildingHit.has_value())
|
||||
{
|
||||
// Actors (ship/station) win over scrap and buildings (REQ-UI-SCRAP-CLICK-SELECT).
|
||||
const BuildingId id = *buildingHit;
|
||||
// A building selection is exclusive: it clears any field selection —
|
||||
// actors and scrap — because buildings win (REQ-UI-SELECTION-CATEGORIES).
|
||||
clearEntitySelection();
|
||||
clearScrapSelection();
|
||||
m_selectedBuildingIds.clear();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
m_selectedEntity = hitEntity;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectedEvent>(hitEntity));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_selectedEntity.has_value())
|
||||
if (ctrl)
|
||||
{
|
||||
m_selectedEntity = std::nullopt;
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectedEvent>(std::nullopt));
|
||||
}
|
||||
|
||||
std::optional<BuildingId> hit = buildingAtTile(tile);
|
||||
if (!hit.has_value())
|
||||
{
|
||||
hit = siteAtTile(tile);
|
||||
}
|
||||
if (hit.has_value())
|
||||
{
|
||||
const BuildingId id = *hit;
|
||||
// A building/construction site outranks scrap (REQ-UI-SCRAP-CLICK-SELECT).
|
||||
clearScrapSelection();
|
||||
if (event->modifiers() & Qt::ControlModifier)
|
||||
bool found = false;
|
||||
std::vector<BuildingId> newSel;
|
||||
for (BuildingId sel : m_selectedBuildingIds)
|
||||
{
|
||||
bool found = false;
|
||||
std::vector<BuildingId> newSel;
|
||||
for (BuildingId sel : m_selectedBuildingIds)
|
||||
{
|
||||
if (sel == id) { found = true; }
|
||||
else { newSel.push_back(sel); }
|
||||
}
|
||||
if (!found) { newSel.push_back(id); }
|
||||
m_selectedBuildingIds = newSel;
|
||||
if (sel == id) { found = true; }
|
||||
else { newSel.push_back(sel); }
|
||||
}
|
||||
else
|
||||
{
|
||||
m_selectedBuildingIds = { id };
|
||||
}
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
}
|
||||
else if (const entt::entity scrapHit =
|
||||
scrapAtWorldPos(m_sim->getAdmin(), 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));
|
||||
if (!found) { newSel.push_back(id); }
|
||||
m_selectedBuildingIds = newSel;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!(event->modifiers() & Qt::ControlModifier))
|
||||
{
|
||||
m_selectedBuildingIds.clear();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
clearScrapSelection();
|
||||
}
|
||||
m_boxSelecting = true;
|
||||
m_boxStartTile = tile;
|
||||
m_boxCurrentTile = tile;
|
||||
m_selectedBuildingIds = { id };
|
||||
}
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
return;
|
||||
}
|
||||
|
||||
// Selecting a field object (actor or scrap) clears any building selection but
|
||||
// lets actors and scrap coexist (REQ-UI-SELECTION-CATEGORIES).
|
||||
const entt::entity actorHit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
|
||||
if (actorHit != entt::null)
|
||||
{
|
||||
if (!m_selectedBuildingIds.empty())
|
||||
{
|
||||
m_selectedBuildingIds.clear();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
}
|
||||
if (ctrl)
|
||||
{
|
||||
// Toggle this actor within the field selection, leaving scrap intact
|
||||
// (REQ-UI-ENTITY-CLICK-SELECT).
|
||||
bool found = false;
|
||||
std::vector<entt::entity> newSel;
|
||||
for (entt::entity sel : m_selectedEntities)
|
||||
{
|
||||
if (sel == actorHit) { found = true; }
|
||||
else { newSel.push_back(sel); }
|
||||
}
|
||||
if (!found) { newSel.push_back(actorHit); }
|
||||
m_selectedEntities = newSel;
|
||||
}
|
||||
else
|
||||
{
|
||||
// A plain click makes this actor the sole selection.
|
||||
m_selectedEntities = { actorHit };
|
||||
clearScrapSelection();
|
||||
}
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectionChangedEvent>(m_selectedEntities));
|
||||
return;
|
||||
}
|
||||
|
||||
if (const entt::entity scrapHit =
|
||||
scrapAtWorldPos(m_sim->getAdmin(), worldPos); scrapHit != entt::null)
|
||||
{
|
||||
if (!m_selectedBuildingIds.empty())
|
||||
{
|
||||
m_selectedBuildingIds.clear();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
}
|
||||
if (ctrl)
|
||||
{
|
||||
// Toggle this pile within the field selection, leaving actors intact
|
||||
// (REQ-UI-SCRAP-MULTI-SELECT).
|
||||
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
|
||||
{
|
||||
// A plain click makes this pile the sole selection.
|
||||
m_selectedScrap = { scrapHit };
|
||||
clearEntitySelection();
|
||||
}
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap));
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty space: a plain click clears the whole selection and starts a box drag;
|
||||
// Ctrl preserves the current selection for additive box-select.
|
||||
if (!ctrl)
|
||||
{
|
||||
if (!m_selectedBuildingIds.empty())
|
||||
{
|
||||
m_selectedBuildingIds.clear();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
}
|
||||
clearEntitySelection();
|
||||
clearScrapSelection();
|
||||
}
|
||||
m_boxSelecting = true;
|
||||
m_boxStartTile = tile;
|
||||
m_boxCurrentTile = tile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2206,8 +2270,9 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
||||
|
||||
if (!boxIds.empty())
|
||||
{
|
||||
// A box covering any building selects buildings; scrap in the box is
|
||||
// ignored (REQ-UI-MULTI-SELECT, REQ-UI-SCRAP-MULTI-SELECT).
|
||||
// A box covering any building selects buildings; field objects (actors and
|
||||
// scrap) in the box are ignored — buildings win (REQ-UI-MULTI-SELECT).
|
||||
clearEntitySelection();
|
||||
clearScrapSelection();
|
||||
if (!ctrl)
|
||||
{
|
||||
@@ -2230,11 +2295,13 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
||||
return;
|
||||
}
|
||||
|
||||
// No buildings in the box: a scrap-only box selects the scrap it covers
|
||||
// (REQ-UI-SCRAP-MULTI-SELECT).
|
||||
// No buildings in the box: select the field objects it covers — ships, defence
|
||||
// stations, and scrap together (REQ-UI-MULTI-SELECT, REQ-UI-SCRAP-MULTI-SELECT).
|
||||
const std::vector<entt::entity> boxActors =
|
||||
actorsInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
||||
const std::vector<entt::entity> boxScrap =
|
||||
scrapInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
||||
if (!boxScrap.empty())
|
||||
if (!boxActors.empty() || !boxScrap.empty())
|
||||
{
|
||||
if (!m_selectedBuildingIds.empty())
|
||||
{
|
||||
@@ -2244,10 +2311,20 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
||||
}
|
||||
if (!ctrl)
|
||||
{
|
||||
m_selectedScrap = boxScrap;
|
||||
m_selectedEntities = boxActors;
|
||||
m_selectedScrap = boxScrap;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (entt::entity e : boxActors)
|
||||
{
|
||||
bool found = false;
|
||||
for (entt::entity sel : m_selectedEntities)
|
||||
{
|
||||
if (sel == e) { found = true; break; }
|
||||
}
|
||||
if (!found) { m_selectedEntities.push_back(e); }
|
||||
}
|
||||
for (entt::entity e : boxScrap)
|
||||
{
|
||||
bool found = false;
|
||||
@@ -2258,6 +2335,8 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
||||
if (!found) { m_selectedScrap.push_back(e); }
|
||||
}
|
||||
}
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EntitySelectionChangedEvent>(m_selectedEntities));
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap));
|
||||
return;
|
||||
@@ -2269,6 +2348,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
||||
m_selectedBuildingIds.clear();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||
clearEntitySelection();
|
||||
clearScrapSelection();
|
||||
}
|
||||
}
|
||||
@@ -2483,6 +2563,8 @@ void GameWorldView::resetForNewGame()
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<DemolishModeChangedEvent>(false));
|
||||
m_selectedBuildingIds.clear();
|
||||
clearEntitySelection();
|
||||
clearScrapSelection();
|
||||
m_copiedConfig = std::nullopt;
|
||||
m_copyConfigFlashes.clear();
|
||||
m_boxSelecting = false;
|
||||
|
||||
Reference in New Issue
Block a user