remove the Shift copy-building-settings gesture

REQ-BLD-COPY-CONFIG is dropped: Shift+right-click no longer caches a
building's recipe / schematic+layout / splitter filters, Shift+left-click
no longer stamps them onto same-type buildings, and the cyan eligible-
target tint and copy/paste flashes go with them. Shift+left-click now
falls through to normal selection, which needs no code of its own -- only
Ctrl is meaningful to selection (REQ-UI-MULTI-SELECT), so dropping the
branch that consumed the click is enough.

readBuildingConfig stays. It had two callers, and the other one is
captureBlueprintFromSelection, which needs it to record each blueprint
building's configuration (REQ-UI-BLUEPRINT-STORAGE). Its five tests stay
too, retagged [blueprint] and rewritten to name the caller that is left.

The visuals.toml copy_config colour is removed together with its
VisualsConfig field and its loader line: overlay keys are mandatory, so
those three have to move as one or startup aborts.

REQ-BLD-COPY-CONFIG-FEEDBACK, cited by four of these files and by the TOML
comment, never existed in requirements.md; deleting the citations retires
a dangling id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
2026-08-06 16:21:53 +02:00
parent 51c6a4a567
commit 648241ba2a
12 changed files with 26 additions and 231 deletions

View File

@@ -157,9 +157,9 @@ Blueprint captureBlueprintFromSelection(const Simulation& sim,
building.type = e.type;
building.rotation = e.rotation;
building.offset = e.anchor - center;
// Recipe / schematic / layout / splitter-filter capture is shared with the
// copy-settings gesture (REQ-BLD-COPY-CONFIG) via readBuildingConfig, which
// handles operational buildings and construction sites alike.
// Recipe / schematic / layout / splitter-filter capture goes through
// readBuildingConfig, which handles operational buildings and construction
// sites alike.
const std::optional<BuildingConfig> config = readBuildingConfig(sim, e.id);
if (config.has_value())
{

View File

@@ -15,8 +15,8 @@ struct BuildingsConfig;
// The user-configurable settings of a single building or construction site: the
// selected recipe / ship schematic, the shipyard module layout, and (for
// splitters) the two output filters. Shared by the copy-settings gesture
// (REQ-BLD-COPY-CONFIG) and blueprint capture (REQ-UI-BLUEPRINT-STORAGE).
// splitters) the two output filters. This is what blueprint capture records per
// constituent building (REQ-UI-BLUEPRINT-STORAGE).
struct BuildingConfig
{
BuildingType type = BuildingType::Miner;

View File

@@ -16,10 +16,10 @@
#include "SimulationTestAccess.h"
#include "TestConfig.h"
// readBuildingConfig underpins the copy-settings gesture (REQ-BLD-COPY-CONFIG):
// it extracts a building's recipe / schematic / layout / splitter filters so they
// can be stamped onto a same-type building. It reads operational buildings and
// construction sites alike.
// readBuildingConfig underpins blueprint capture (REQ-UI-BLUEPRINT-STORAGE): it
// extracts a building's recipe / schematic / layout / splitter filters so they can be
// stored in the blueprint and reapplied on placement. It reads operational buildings
// and construction sites alike.
namespace
{
@@ -60,7 +60,7 @@ const ShipDef* findAvailableSchematic(const GameConfig& cfg)
}
} // namespace
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]")
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
Simulation sim(loadTestConfig(), 7);
@@ -78,7 +78,7 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]"
}
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
"[copyconfig]")
"[blueprint]")
{
const GameConfig cfg = loadTestConfig();
Simulation sim(loadTestConfig(), 7);
@@ -93,7 +93,7 @@ TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
}
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
"[copyconfig]")
"[blueprint]")
{
const GameConfig cfg = loadTestConfig();
Simulation sim(loadTestConfig(), 7);
@@ -113,7 +113,7 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
CHECK(config->shipLayout.has_value());
}
TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
TEST_CASE("readBuildingConfig reads a queued construction site", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
Simulation sim(loadTestConfig(), 7);
@@ -134,7 +134,7 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
CHECK(*config->recipeId == "mine_iron_ore");
}
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]")
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[blueprint]")
{
Simulation sim(loadTestConfig(), 7);
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());

View File

@@ -269,20 +269,6 @@ void GameWorldView::onFrame()
pruneDespawnedDebris();
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
// while the game is paused (REQ-BLD-COPY-CONFIG-FEEDBACK).
if (!m_copyConfigFlashes.empty())
{
std::vector<CopyConfigFlash> live;
for (CopyConfigFlash flash : m_copyConfigFlashes)
{
flash.remainingMs -= elapsed;
if (flash.remainingMs > 0) { live.push_back(flash); }
}
m_copyConfigFlashes = std::move(live);
}
// Apply held scroll
{
const bool viewMoved =
@@ -414,9 +400,8 @@ void GameWorldView::paintGL()
WorldRenderFrame GameWorldView::makeRenderFrame() const
{
return WorldRenderFrame{m_selection, m_buildMode, m_activeBeams, m_copiedConfig,
m_copyConfigFlashes, m_boxSelecting, m_boxStartTile,
m_boxCurrentTile, m_debugDraw};
return WorldRenderFrame{m_selection, m_buildMode, m_activeBeams, m_boxSelecting,
m_boxStartTile, m_boxCurrentTile, m_debugDraw};
}
// ---------------------------------------------------------------------------
@@ -1026,10 +1011,6 @@ void GameWorldView::keyReleaseEvent(QKeyEvent* event)
return;
}
if (m_inputMapper.handleKeyRelease(event)) { return; }
// Releasing Shift discards the copied building settings (REQ-BLD-COPY-CONFIG).
// Stays here rather than moving to the input mapper: Shift is the modifier of a
// mouse gesture, not a keyboard action of its own.
if (event->key() == Qt::Key_Shift) { m_copiedConfig.reset(); }
QOpenGLWidget::keyReleaseEvent(event);
}
@@ -1061,15 +1042,6 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
{
m_buildMode.exitCurrentMode();
}
else if (event->modifiers() & Qt::ShiftModifier)
{
// Shift + right-click copies a building's settings, but only in the
// default selection mode (REQ-BLD-COPY-CONFIG).
const QPoint tile = coordinates.widgetToTile(event->pos());
std::optional<BuildingId> id = buildingAtTile(tile);
if (!id.has_value()) { id = siteAtTile(tile); }
if (id.has_value()) { copyConfigFrom(*id); }
}
}
return;
}
@@ -1105,20 +1077,6 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
}
else
{
// Shift + left-click applies the copied settings to a same-type building
// (REQ-BLD-COPY-CONFIG). Consumes the click so it does not change the
// selection. Only active in the default selection mode.
if ((event->modifiers() & Qt::ShiftModifier) && m_copiedConfig.has_value())
{
std::optional<BuildingId> id = buildingAtTile(tile);
if (!id.has_value()) { id = siteAtTile(tile); }
if (id.has_value())
{
pasteConfigTo(*id);
return;
}
}
const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0;
// Only a click that hit nothing starts a box drag. Starting one on a hit
@@ -1375,85 +1333,6 @@ void GameWorldView::rotateGhost(bool clockwise)
}
}
void GameWorldView::copyConfigFrom(BuildingId id)
{
const std::optional<BuildingConfig> config = readBuildingConfig(*m_sim, id);
if (!config.has_value()) { return; }
// Only cache when there is something to copy: a selected recipe / schematic
// (Miner, Assembler, Shipyard) or any Splitter (empty filters = accept-all).
// Building types with no settings (Smelter, Reprocessing Plant, Salvage Bay,
// belts / tunnels, HQ) leave any existing cache untouched (REQ-BLD-COPY-CONFIG).
if (!config->recipeId.has_value() && !config->isSplitter) { return; }
m_copiedConfig = config;
m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs });
}
void GameWorldView::pasteConfigTo(BuildingId id)
{
if (!m_copiedConfig.has_value()) { return; }
const std::optional<BuildingConfig> target = readBuildingConfig(*m_sim, id);
if (!target.has_value() || target->type != m_copiedConfig->type) { return; }
// The paste applies below; flash the target to confirm (REQ-BLD-COPY-CONFIG-FEEDBACK).
m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs });
const BuildingConfig& source = *m_copiedConfig;
// The cached settings were valid on a same-type source building, so they are
// valid and available on the target: unlock state is global, so a selected
// recipe / schematic (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SCHEMATIC) and splitter
// filter item types (REQ-LOCK-UI-SPLITTER) remain unlocked. Applying reuses the
// same configuration commands as the selected building panel, inheriting their
// buffer-clearing and mid-cycle-cancel semantics (REQ-MAT-INPUT-BUFFER,
// REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
if (source.isSplitter)
{
// Operational splitters are configured by tile; sites by BuildingId
// (mirrors SelectedBuildingPanel::onSplitterFilterChanged).
if (const Building* building = findBuilding(m_sim->getFactoryState(), id))
{
std::shared_ptr<SetSplitterFiltersCommand> command =
std::make_shared<SetSplitterFiltersCommand>();
command->tile = building->anchor;
command->filterA = source.splitterFilterA;
command->filterB = source.splitterFilterB;
enqueueCommand(command);
}
else
{
std::shared_ptr<SetSiteSplitterFiltersCommand> command =
std::make_shared<SetSiteSplitterFiltersCommand>();
command->id = id;
command->filterA = source.splitterFilterA;
command->filterB = source.splitterFilterB;
enqueueCommand(command);
}
return;
}
if (source.recipeId.has_value())
{
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
command->id = id;
command->recipeId = *source.recipeId;
enqueueCommand(command);
}
// For a shipyard the schematic (above) must be applied before its module
// layout, so both commands drain in order at the next tick boundary.
if (source.type == BuildingType::Shipyard && source.shipLayout.has_value())
{
std::shared_ptr<SetShipLayoutCommand> command =
std::make_shared<SetShipLayoutCommand>();
command->id = id;
command->layout = *source.shipLayout;
enqueueCommand(command);
}
}
double GameWorldView::getGameSpeed() const
{
return m_gameSpeedMultiplier;
@@ -1484,8 +1363,6 @@ void GameWorldView::resetForNewGame()
m_activeBeams.clear();
m_schematicChoiceShown = false;
m_selection.clearAll();
m_copiedConfig = std::nullopt;
m_copyConfigFlashes.clear();
m_boxSelecting = false;
m_camera.reset();
// Drops any key still held across the restart, which also republishes the pan

View File

@@ -20,7 +20,6 @@
#include "Blueprint.h"
#include "BuildModeController.h"
#include "BuildingConfig.h"
#include "BlueprintModeExitedEvent.h"
#include "BlueprintPlacementRequestedEvent.h"
#include "BuilderModeExitedEvent.h"
@@ -217,12 +216,6 @@ private:
// Enqueues placements and rotate-in-place commands for the resolved path.
void applyBeltDragPath();
// Copy-settings gesture (REQ-BLD-COPY-CONFIG): Shift+right-click copies a
// building's configuration into m_copiedConfig; Shift+left-click applies it to
// another building of the same type via the existing configuration commands.
void copyConfigFrom(BuildingId id);
void pasteConfigTo(BuildingId id);
// Turns the ghost and refreshes everything that depends on its facing: placement
// validity, the tunnel completion match, and an in-progress belt drag's path.
// The mode transitions themselves live on m_buildMode.
@@ -271,17 +264,6 @@ private:
// and to resolve the tunnel ghost sub-tile (REQ-BLD-TUNNEL-MODE).
QVector2D m_cursorWorldPos;
// Temporary cache for the copy-settings gesture (REQ-BLD-COPY-CONFIG); held
// only while Shift is down and cleared on Shift release.
std::optional<BuildingConfig> m_copiedConfig;
// Brief outline flash shown on a building when settings are copied from it or
// pasted onto it (REQ-BLD-COPY-CONFIG-FEEDBACK). remainingMs counts down in
// wall-clock time so the flash plays at a fixed length regardless of game speed
// (and while paused).
std::vector<CopyConfigFlash> m_copyConfigFlashes;
static constexpr qint64 kCopyFlashDurationMs = 300;
bool m_debugDraw;
// Owns the selection across all three categories and the rules for moving

View File

@@ -48,7 +48,6 @@ struct OverlayVisuals
QColor selectionRect;
QColor tileHighlight;
QColor selectedOutline;
QColor copyConfig;
QColor lockedAsteroid;
QColor modalDim;
QColor tunnelPreview; // tunnel connection preview highlight (REQ-BLD-TUNNEL-MODE)

View File

@@ -224,7 +224,6 @@ VisualsConfig VisualsLoader::load(const std::string& path)
cfg.overlays.selectionRect = parseColor(requireString(ov, "selection_rect", "overlays"), "overlays.selection_rect");
cfg.overlays.tileHighlight = parseColor(requireString(ov, "tile_highlight", "overlays"), "overlays.tile_highlight");
cfg.overlays.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline");
cfg.overlays.copyConfig = parseColor(requireString(ov, "copy_config", "overlays"), "overlays.copy_config");
cfg.overlays.lockedAsteroid = parseColor(requireString(ov, "locked_asteroid", "overlays"), "overlays.locked_asteroid");
cfg.overlays.modalDim = parseColor(requireString(ov, "modal_dim", "overlays"), "overlays.modal_dim");
cfg.overlays.tunnelPreview = parseColor(requireString(ov, "tunnel_preview", "overlays"), "overlays.tunnel_preview");

View File

@@ -129,7 +129,6 @@ void WorldRenderer::render(QPainter& painter, const WorldCoordinates& coordinate
// into the port and stay visible while crossing directly between two touching
// buildings (REQ-MAT-OUTPUT-EMERGE, REQ-MAT-INPUT-INTAKE, REQ-MAT-DIRECT-COUPLE).
drawPortItems(painter, coordinates, frame);
drawCopyConfigFeedback(painter, coordinates, frame);
drawStations(painter, coordinates, frame);
drawBeltItems(painter, coordinates, frame);
drawDebris(painter, coordinates, frame);
@@ -505,44 +504,6 @@ void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordi
}
}
void WorldRenderer::drawCopyConfigFeedback(QPainter& painter, const WorldCoordinates& coordinates,
const WorldRenderFrame& frame)
{
const QColor color = m_visuals.overlays.copyConfig;
// Eligible-target tint: while a configuration is cached, every same-type
// building and site (the source included) is a valid paste target and is
// washed in the copy-settings color (REQ-BLD-COPY-CONFIG-FEEDBACK).
if (frame.copiedConfig.has_value())
{
painter.setPen(Qt::NoPen);
painter.setBrush(color);
const BuildingType type = frame.copiedConfig->type;
for (const Building& b : getAllBuildings(m_sim.getFactoryState()))
{
if (b.type != type) { continue; }
const std::optional<QRectF> rect = footprintWidgetRect(coordinates, b.id);
if (rect.has_value()) { painter.drawRect(*rect); }
}
for (const ConstructionSite& s : getAllSites(m_sim.getFactoryState()))
{
if (s.type != type) { continue; }
const std::optional<QRectF> rect = footprintWidgetRect(coordinates, s.id);
if (rect.has_value()) { painter.drawRect(*rect); }
}
}
// Copy / paste flashes: a brief outline in the same color, drawn like the
// selection outline (REQ-BLD-COPY-CONFIG-FEEDBACK).
painter.setPen(QPen(color, 2));
painter.setBrush(Qt::NoBrush);
for (const CopyConfigFlash& flash : frame.copyConfigFlashes)
{
const std::optional<QRectF> rect = footprintWidgetRect(coordinates, flash.id);
if (rect.has_value()) { painter.drawRect(rect->adjusted(-1, -1, 1, 1)); }
}
}
void WorldRenderer::drawPortItems(QPainter& painter, const WorldCoordinates& coordinates,
const WorldRenderFrame& /*frame*/)
{

View File

@@ -14,7 +14,6 @@
#include "BeamFiredEvent.h"
#include "BuildModeController.h"
#include "BuildingConfig.h"
#include "BuildingId.h"
#include "BuildingType.h"
#include "Rotation.h"
@@ -36,31 +35,19 @@ struct ActiveBeam
QVector2D targetOffset;
};
// Brief outline flash shown on a building when settings are copied from it or
// pasted onto it (REQ-BLD-COPY-CONFIG-FEEDBACK). remainingMs counts down in
// wall-clock time so the flash plays at a fixed length regardless of game speed
// (and while paused).
struct CopyConfigFlash
{
BuildingId id;
qint64 remainingMs;
};
// Everything the renderer draws that the simulation does not know about: what the
// player has selected, which build mode is active, and the other transient bits of
// interaction state the view owns. Assembled fresh each frame and passed by
// reference, so the renderer holds no copy that could go stale.
struct WorldRenderFrame
{
const SelectionController& selection;
const BuildModeController& buildMode;
const std::vector<ActiveBeam>& beams;
const std::optional<BuildingConfig>& copiedConfig;
const std::vector<CopyConfigFlash>& copyConfigFlashes;
bool isBoxSelecting;
QPoint boxStartTile;
QPoint boxCurrentTile;
bool isDebugDrawEnabled;
const SelectionController& selection;
const BuildModeController& buildMode;
const std::vector<ActiveBeam>& beams;
bool isBoxSelecting;
QPoint boxStartTile;
QPoint boxCurrentTile;
bool isDebugDrawEnabled;
};
// Draws the game world: terrain, buildings, items, ships, effects and the build
@@ -94,8 +81,6 @@ private:
const WorldRenderFrame& frame);
void drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates,
const WorldRenderFrame& frame);
void drawCopyConfigFeedback(QPainter& painter, const WorldCoordinates& coordinates,
const WorldRenderFrame& frame);
void drawPortItems(QPainter& painter, const WorldCoordinates& coordinates,
const WorldRenderFrame& frame);
void drawStations(QPainter& painter, const WorldCoordinates& coordinates,
@@ -145,8 +130,7 @@ private:
const QColor& fill) const;
// Widget-space rectangle covering a building or construction site's footprint,
// or nullopt if the id resolves to neither. Shared by the selection highlight
// and the copy-settings feedback (REQ-BLD-COPY-CONFIG-FEEDBACK).
// or nullopt if the id resolves to neither. Used by the selection highlight.
std::optional<QRectF> footprintWidgetRect(const WorldCoordinates& coordinates,
BuildingId id) const;