From f11db0c072b5dbd6fe23aa58bfd771f5c01a6372 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Wed, 8 Jul 2026 21:04:59 +0200 Subject: [PATCH] Add demolish box-drag interaction --- docs/requirements.md | 2 + src/ui/GameWorldView.cpp | 126 ++++++++++++++++++++++++--------------- src/ui/GameWorldView.h | 3 + 3 files changed, 84 insertions(+), 47 deletions(-) diff --git a/docs/requirements.md b/docs/requirements.md index 9c524b3..b28d900 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -109,6 +109,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des - REQ-BLD-BELT-DRAG: For belts, the player can click and drag across multiple tiles to place a construction site on each tile in one gesture. - REQ-BLD-TUNNEL-AUTO-SWITCH: After the player successfully places a Tunnel Entry construction site, builder mode automatically switches to Tunnel Exit (and vice versa), preserving the current ghost rotation. This makes it easy to immediately place the paired end without manually selecting the complementary type. - REQ-BLD-DEMOLISH: The player can demolish a placed factory building. Demolition returns `world.toml [world].refund_percentage` percent of the original building block cost (default 75%) to the global stock. Exception: if the building is still in the construction queue (not yet fully built, including the one currently being constructed), it is removed from the queue and the **full** building block cost is refunded. The HQ and player defence stations cannot be demolished. +- REQ-BLD-DEMOLISH-CLICK: While in demolish mode (REQ-UI-HOTKEYS, REQ-UI-DEMOLISH-BUTTON), left-clicking a placed factory building or construction site in the game world demolishes it, following the refund rules of REQ-BLD-DEMOLISH — the partial refund for built buildings and the full refund for still-queued construction sites. Clicking a building that cannot be demolished (the HQ or a player defence station, per REQ-BLD-DEMOLISH), or clicking empty world space, has no effect. Demolish mode stays active after a demolition so the player can demolish further buildings without re-entering the mode; it is exited via the Q toggle (REQ-UI-HOTKEYS) or the Demolish button (REQ-UI-DEMOLISH-BUTTON). +- REQ-BLD-DEMOLISH-BOX: While in demolish mode (REQ-UI-HOTKEYS, REQ-UI-DEMOLISH-BUTTON), the player can click and drag a selection box in the game world. A selection rectangle is drawn while dragging, using the same box-drag gesture and coverage semantics as the multi-select box (REQ-UI-MULTI-SELECT). On mouse up, every placed factory building and construction site covered by the box is demolished, each following the refund rules of REQ-BLD-DEMOLISH — the partial refund for built buildings and the full refund for still-queued construction sites. Buildings that cannot be demolished (the HQ and player defence stations, per REQ-BLD-DEMOLISH) are excluded from the box demolition; ships and defence stations are never affected. - REQ-BLD-SITE-CONFIG: A construction site — a building that has been placed but is still queued or under construction (REQ-BLD-QUEUE) — can be selected and configured exactly like the equivalent operational building, before it finishes building. Whatever configuration the building type supports is available on the site: the recipe for a Miner or Assembler (REQ-UI-SELECT-BUTTON), the produced-ship schematic and its module layout for a Shipyard (REQ-UI-SELECT-BUTTON, REQ-MOD-UI-PREVIEW, REQ-MOD-UI-DIALOG), and the output filters for a Splitter (REQ-BLD-SPLITTER) — all set through the same Selected Building Panel controls (REQ-UI-CONFIG-INLINE). Only currently unlocked recipes and schematics are offered, exactly as for operational buildings (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SCHEMATIC, REQ-LOCK-UI-SPLITTER). The configuration is stored on the construction site and carries over unchanged when construction completes, so the building becomes operational already configured. A construction site has no input/output buffers and runs no production cycle, so the buffer and production-progress portions of the panel (REQ-UI-SINGLE-SELECTION, REQ-UI-PRODUCTION-PROGRESS) are not shown for it; only its construction progress (REQ-UI-CONSTRUCTION-PROGRESS) and its configuration controls appear. (Blueprint placement already applies a stored recipe or schematic to a construction site on placement per REQ-UI-BLUEPRINT-PLACE; this requirement additionally lets the player set or change that configuration directly on an existing site.) ## Building Types diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 692b73f..1abd308 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -570,6 +570,41 @@ BuildingId GameWorldView::siteAtTile(QPoint tile) const } +std::vector 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 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 GameWorldView::entityPosition(entt::entity entity) const { if (!m_sim->admin().isValid(entity) || !m_sim->admin().hasAll(entity)) @@ -1236,8 +1271,28 @@ void GameWorldView::drawOverlays(QPainter& painter) } } - // Demolish hover tint - if (m_demolishMode && m_demolishHoverBuildingId != kInvalidBuildingId) + // Demolish tint: while dragging a demolish box, tint every covered + // 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* 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); if (b) @@ -1531,24 +1586,11 @@ void GameWorldView::mousePressEvent(QMouseEvent* event) } else if (m_demolishMode) { - BuildingId hovered = buildingAtTile(tile); - if (hovered == kInvalidBuildingId) - { - hovered = siteAtTile(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 command = - std::make_shared(); - command->id = hovered; - enqueueCommand(command); - m_demolishHoverBuildingId = kInvalidBuildingId; - } - } + // Start a demolish box drag; a plain click resolves as a 1x1 box on + // release (REQ-BLD-DEMOLISH-CLICK, REQ-BLD-DEMOLISH-BOX). + m_boxSelecting = true; + m_boxStartTile = tile; + m_boxCurrentTile = tile; } else { @@ -1636,6 +1678,7 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event) else if (m_demolishMode) { m_demolishHoverBuildingId = buildingAtTile(tile); + if (m_boxSelecting) { m_boxCurrentTile = tile; } } else if (m_boxSelecting) { @@ -1657,44 +1700,33 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event) { m_boxSelecting = false; - const int x0 = std::min(m_boxStartTile.x(), m_boxCurrentTile.x()); - const int y0 = std::min(m_boxStartTile.y(), m_boxCurrentTile.y()); - const int x1 = std::max(m_boxStartTile.x(), m_boxCurrentTile.x()); - const int y1 = std::max(m_boxStartTile.y(), m_boxCurrentTile.y()); + const std::vector boxIds = + buildingsInBox(m_boxStartTile, m_boxCurrentTile); - std::vector boxSel; - for (const Building& b : m_sim->buildings().allBuildings()) + if (m_demolishMode) { - 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 - && cell.y() >= y0 && cell.y() <= y1) - { - boxSel.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) - { - boxSel.push_back(s.id); - break; - } + const Building* b = m_sim->buildings().findBuilding(id); + if (b && b->type == BuildingType::Hq) { continue; } + std::shared_ptr command = + std::make_shared(); + command->id = id; + enqueueCommand(command); } + m_demolishHoverBuildingId = kInvalidBuildingId; + return; } if (!(event->modifiers() & Qt::ControlModifier)) { - m_selectedBuildingIds = boxSel; + m_selectedBuildingIds = boxIds; } else { - for (BuildingId id : boxSel) + for (BuildingId id : boxIds) { bool found = false; for (BuildingId sel : m_selectedBuildingIds) diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index b8dc0c7..22cde0a 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -143,6 +143,9 @@ private: const BuildingDef* findBuildingDef(BuildingType type) const; BuildingId buildingAtTile(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 buildingsInBox(QPoint cornerA, QPoint cornerB) const; QVector2D widgetToWorld(QPoint widgetPt) const; void drawPortGlyph(QPainter& painter, QPoint bodyTile,