Implement box-drag demolish in demolish mode

Add REQ-BLD-DEMOLISH-BOX: while in demolish mode, click-and-drag a
selection box and demolish every covered building and construction site
on mouse-up. Unify the single-click demolish (REQ-BLD-DEMOLISH-CLICK)
into the same path so a click is a 1x1 box that resolves on release.

- Reuse the existing box-select state and rectangle render; branch on
  demolish mode in mouseReleaseEvent to demolish instead of select.
- Extract buildingsInBox() helper shared by the select-release,
  demolish-release, and demolish-drag tint paths (removes the inline
  box-intersection loop duplicated in mouseReleaseEvent).
- Tint all covered buildings/sites during the demolish drag; HQ is
  excluded from demolition per REQ-BLD-DEMOLISH.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DyCu8vwChKMbLJQ3xosYEN
This commit is contained in:
2026-07-08 21:00:52 +02:00
parent ce8e524317
commit 4c4e027cf6
2 changed files with 82 additions and 47 deletions

View File

@@ -570,6 +570,41 @@ BuildingId GameWorldView::siteAtTile(QPoint tile) const
} }
std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cornerB) const
{
const int x0 = std::min(cornerA.x(), cornerB.x());
const int y0 = std::min(cornerA.y(), cornerB.y());
const int x1 = std::max(cornerA.x(), cornerB.x());
const int y1 = std::max(cornerA.y(), cornerB.y());
std::vector<BuildingId> ids;
for (const Building& b : m_sim->buildings().allBuildings())
{
for (const QPoint& cell : b.bodyCells)
{
if (cell.x() >= x0 && cell.x() <= x1
&& cell.y() >= y0 && cell.y() <= y1)
{
ids.push_back(b.id);
break;
}
}
}
for (const ConstructionSite& s : m_sim->buildings().allSites())
{
for (const QPoint& cell : s.bodyCells)
{
if (cell.x() >= x0 && cell.x() <= x1
&& cell.y() >= y0 && cell.y() <= y1)
{
ids.push_back(s.id);
break;
}
}
}
return ids;
}
std::optional<QVector2D> GameWorldView::entityPosition(entt::entity entity) const std::optional<QVector2D> GameWorldView::entityPosition(entt::entity entity) const
{ {
if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<PositionComponent>(entity)) if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll<PositionComponent>(entity))
@@ -1236,8 +1271,28 @@ void GameWorldView::drawOverlays(QPainter& painter)
} }
} }
// Demolish hover tint // Demolish tint: while dragging a demolish box, tint every covered
if (m_demolishMode && m_demolishHoverBuildingId != kInvalidBuildingId) // building/site (REQ-BLD-DEMOLISH-BOX); otherwise tint the hovered one.
if (m_demolishMode && m_boxSelecting)
{
for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile))
{
const Building* b = m_sim->buildings().findBuilding(id);
if (b && b->type == BuildingType::Hq) { continue; }
const std::vector<QPoint>* cells = nullptr;
const ConstructionSite* s = nullptr;
if (b) { cells = &b->bodyCells; }
else if ((s = m_sim->buildings().findSite(id))) { cells = &s->bodyCells; }
if (cells)
{
for (const QPoint& cell : *cells)
{
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
}
}
}
}
else if (m_demolishMode && m_demolishHoverBuildingId != kInvalidBuildingId)
{ {
const Building* b = m_sim->buildings().findBuilding(m_demolishHoverBuildingId); const Building* b = m_sim->buildings().findBuilding(m_demolishHoverBuildingId);
if (b) if (b)
@@ -1531,24 +1586,11 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
} }
else if (m_demolishMode) else if (m_demolishMode)
{ {
BuildingId hovered = buildingAtTile(tile); // Start a demolish box drag; a plain click resolves as a 1x1 box on
if (hovered == kInvalidBuildingId) // release (REQ-BLD-DEMOLISH-CLICK, REQ-BLD-DEMOLISH-BOX).
{ m_boxSelecting = true;
hovered = siteAtTile(tile); m_boxStartTile = tile;
} m_boxCurrentTile = tile;
if (hovered != kInvalidBuildingId)
{
const Building* b = m_sim->buildings().findBuilding(hovered);
const bool isProtected = b && b->type == BuildingType::Hq;
if (!isProtected)
{
std::shared_ptr<DemolishCommand> command =
std::make_shared<DemolishCommand>();
command->id = hovered;
enqueueCommand(command);
m_demolishHoverBuildingId = kInvalidBuildingId;
}
}
} }
else else
{ {
@@ -1636,6 +1678,7 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
else if (m_demolishMode) else if (m_demolishMode)
{ {
m_demolishHoverBuildingId = buildingAtTile(tile); m_demolishHoverBuildingId = buildingAtTile(tile);
if (m_boxSelecting) { m_boxCurrentTile = tile; }
} }
else if (m_boxSelecting) else if (m_boxSelecting)
{ {
@@ -1657,44 +1700,33 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
{ {
m_boxSelecting = false; m_boxSelecting = false;
const int x0 = std::min(m_boxStartTile.x(), m_boxCurrentTile.x()); const std::vector<BuildingId> boxIds =
const int y0 = std::min(m_boxStartTile.y(), m_boxCurrentTile.y()); buildingsInBox(m_boxStartTile, m_boxCurrentTile);
const int x1 = std::max(m_boxStartTile.x(), m_boxCurrentTile.x());
const int y1 = std::max(m_boxStartTile.y(), m_boxCurrentTile.y());
std::vector<BuildingId> boxSel; if (m_demolishMode)
for (const Building& b : m_sim->buildings().allBuildings())
{ {
for (const QPoint& cell : b.bodyCells) // Demolish every covered building/site; the HQ is protected
// (REQ-BLD-DEMOLISH, REQ-BLD-DEMOLISH-BOX).
for (BuildingId id : boxIds)
{ {
if (cell.x() >= x0 && cell.x() <= x1 const Building* b = m_sim->buildings().findBuilding(id);
&& cell.y() >= y0 && cell.y() <= y1) if (b && b->type == BuildingType::Hq) { continue; }
{ std::shared_ptr<DemolishCommand> command =
boxSel.push_back(b.id); std::make_shared<DemolishCommand>();
break; command->id = id;
} enqueueCommand(command);
}
}
for (const ConstructionSite& s : m_sim->buildings().allSites())
{
for (const QPoint& cell : s.bodyCells)
{
if (cell.x() >= x0 && cell.x() <= x1
&& cell.y() >= y0 && cell.y() <= y1)
{
boxSel.push_back(s.id);
break;
}
} }
m_demolishHoverBuildingId = kInvalidBuildingId;
return;
} }
if (!(event->modifiers() & Qt::ControlModifier)) if (!(event->modifiers() & Qt::ControlModifier))
{ {
m_selectedBuildingIds = boxSel; m_selectedBuildingIds = boxIds;
} }
else else
{ {
for (BuildingId id : boxSel) for (BuildingId id : boxIds)
{ {
bool found = false; bool found = false;
for (BuildingId sel : m_selectedBuildingIds) for (BuildingId sel : m_selectedBuildingIds)

View File

@@ -143,6 +143,9 @@ private:
const BuildingDef* findBuildingDef(BuildingType type) const; const BuildingDef* findBuildingDef(BuildingType type) const;
BuildingId buildingAtTile(QPoint tile) const; BuildingId buildingAtTile(QPoint tile) const;
BuildingId siteAtTile(QPoint tile) const; BuildingId siteAtTile(QPoint tile) const;
// Ids of all buildings and construction sites whose footprint intersects
// the tile box spanned by the two (unordered) corner tiles.
std::vector<BuildingId> buildingsInBox(QPoint cornerA, QPoint cornerB) const;
QVector2D widgetToWorld(QPoint widgetPt) const; QVector2D widgetToWorld(QPoint widgetPt) const;
void drawPortGlyph(QPainter& painter, QPoint bodyTile, void drawPortGlyph(QPainter& painter, QPoint bodyTile,