Implement REQ-BLD-COPY-CONFIG: copy building settings with Shift

Shift+right-click a building copies its settings (miner/assembler recipe,
shipyard schematic + module layout, or splitter output filters) into a
temporary UI cache; Shift+left-click stamps them onto another building of
the same type. The gesture is active only in the default selection mode,
works on operational buildings and construction sites, and the cache
clears when Shift is released (or on Restart).

Paste reuses the existing configuration commands (SetRecipe, SetShipLayout,
SetSplitterFilters, SetSiteSplitterFilters), so it inherits their buffer-
clearing / mid-cycle-cancel semantics and replays for free — no new command
kinds or serializer changes.

Extract the recipe/schematic/layout/filter reader into a shared
readBuildingConfig() (lib/sim), handling buildings and sites alike, and
reuse it from BlueprintPanel to remove the duplicated capture logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
This commit is contained in:
2026-07-09 16:21:59 +02:00
parent c144078ea6
commit 92cfcee682
8 changed files with 352 additions and 13 deletions

View File

@@ -1617,6 +1617,8 @@ void GameWorldView::keyReleaseEvent(QKeyEvent* event)
}
if (event->key() == Qt::Key_A) { m_scrollLeft = false; }
if (event->key() == Qt::Key_D) { m_scrollRight = false; }
// Releasing Shift discards the copied building settings (REQ-BLD-COPY-CONFIG).
if (event->key() == Qt::Key_Shift) { m_copiedConfig.reset(); }
QOpenGLWidget::keyReleaseEvent(event);
}
@@ -1629,6 +1631,15 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
if (m_builderType.has_value()) { exitBuilderMode(); }
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
else if (m_demolishMode) { toggleDemolishMode(); }
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 = widgetToTile(event->pos());
BuildingId id = buildingAtTile(tile);
if (id == kInvalidBuildingId) { id = siteAtTile(tile); }
if (id != kInvalidBuildingId) { copyConfigFrom(id); }
}
}
return;
}
@@ -1663,6 +1674,20 @@ 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())
{
BuildingId id = buildingAtTile(tile);
if (id == kInvalidBuildingId) { id = siteAtTile(tile); }
if (id != kInvalidBuildingId)
{
pasteConfigTo(id);
return;
}
}
const QVector2D worldPos = widgetToWorld(event->pos());
const entt::entity hitEntity = entityAtWorldPos(m_sim->admin(), worldPos);
@@ -1868,6 +1893,81 @@ 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;
}
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; }
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 = m_sim->buildings().findBuilding(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);
}
}
void GameWorldView::enterBuilderMode(BuildingType type)
{
m_builderType = type;
@@ -1940,6 +2040,7 @@ void GameWorldView::resetForNewGame()
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(false));
m_selectedBuildingIds.clear();
m_copiedConfig = std::nullopt;
m_boxSelecting = false;
m_scrollXTiles = 0.0f;
m_scrollLeft = false;