re-cover copy-settings through single-building blueprints

This commit is contained in:
2026-08-06 19:44:55 +02:00
parent 9a3b6c10d6
commit 08d8b0dd90
13 changed files with 542 additions and 65 deletions

View File

@@ -563,16 +563,21 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
// Locked building types are excluded from this placement entirely
// (REQ-LOCK-BUILDING, REQ-LOCK-UI-BLUEPRINT): not validity-checked here.
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
if (!canPlaceBuildingHere(bb.type, center + bb.offset, bb.rotation)) { return; }
if (resolveBlueprintGhostHere(bb, center).action == BlueprintGhostAction::Invalid)
{
return;
}
}
// Cost only applies to buildings that are genuinely new (not rotate-in-place),
// and excludes locked building types (REQ-LOCK-UI-BLUEPRINT).
// Only genuinely new buildings are charged for: a compatible overlap places nothing
// (REQ-UI-BLUEPRINT-OVERLAP) and a transfer changes settings only
// (REQ-UI-BLUEPRINT-TRANSFER). Locked building types are excluded from the total
// as well (REQ-LOCK-UI-BLUEPRINT).
int totalCost = 0;
for (const BlueprintBuilding& bb : bp.buildings)
{
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
if (findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), bb.type, center + bb.offset, bb.rotation).has_value())
if (resolveBlueprintGhostHere(bb, center).action != BlueprintGhostAction::PlaceNew)
{
continue;
}
@@ -585,15 +590,15 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
{
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
const QPoint anchor = center + bb.offset;
const std::optional<BuildingId> rotateTarget =
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), bb.type, anchor, bb.rotation);
if (rotateTarget.has_value())
const BlueprintGhostResolved resolved = resolveBlueprintGhostHere(bb, center);
// The building the blueprint wants is already there, facing the same way: leave
// it exactly as it is (REQ-UI-BLUEPRINT-OVERLAP).
if (resolved.action == BlueprintGhostAction::CompatibleOverlap) { continue; }
if (resolved.action == BlueprintGhostAction::Transfer)
{
std::shared_ptr<RotateInPlaceCommand> rotateCommand =
std::make_shared<RotateInPlaceCommand>();
rotateCommand->id = *rotateTarget;
rotateCommand->newRotation = bb.rotation;
enqueueCommand(rotateCommand);
transferConfigTo(*resolved.targetId, bb);
continue;
}
@@ -606,26 +611,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
command->anchor = anchor;
command->rotation = bb.rotation;
if (!bb.recipeId.empty())
{
if (bb.type == BuildingType::Shipyard)
{
if (m_sim->isSchematicUnlocked(bb.recipeId))
{
command->recipeId = bb.recipeId;
}
}
else
{
const bool needsUnlockCheck = bb.type == BuildingType::Miner
|| bb.type == BuildingType::Assembler;
if (!needsUnlockCheck || m_sim->isRecipeUnlocked(bb.recipeId))
{
command->recipeId = bb.recipeId;
}
}
}
command->recipeId = unlockedRecipeId(bb);
command->shipLayout = bb.shipLayout;
if (bb.type == BuildingType::Splitter
@@ -643,6 +629,93 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
}
}
BlueprintGhostResolved GameWorldView::resolveBlueprintGhostHere(const BlueprintBuilding& building,
QPoint center) const
{
// The one-building test reads the blueprint as stored, before locked types are
// dropped, so the gesture behaves the same however much the player has unlocked
// (REQ-UI-BLUEPRINT-TRANSFER).
return resolveBlueprintGhost(m_sim->getFactoryState(), m_sim->getConfig(),
building.type, center + building.offset, building.rotation,
m_buildMode.getBlueprint().buildings.size() == 1);
}
std::string GameWorldView::unlockedRecipeId(const BlueprintBuilding& building) const
{
// A stored recipe or schematic is applied only while it is unlocked; a locked one
// yields no id at all rather than a stale one (REQ-LOCK-UI-BLUEPRINT). Shared by
// placement and configuration transfer so the two cannot gate differently.
if (building.recipeId.empty()) { return std::string(); }
if (building.type == BuildingType::Shipyard)
{
return m_sim->isSchematicUnlocked(building.recipeId) ? building.recipeId
: std::string();
}
const bool needsUnlockCheck = building.type == BuildingType::Miner
|| building.type == BuildingType::Assembler;
if (!needsUnlockCheck || m_sim->isRecipeUnlocked(building.recipeId))
{
return building.recipeId;
}
return std::string();
}
void GameWorldView::transferConfigTo(BuildingId id, const BlueprintBuilding& source)
{
// Hands the blueprint's settings to a building that is already there, changing
// nothing else -- no construction site, no cost, no rotation
// (REQ-UI-BLUEPRINT-TRANSFER). Every field is sent unconditionally, so a field the
// blueprint has nothing stored for clears the target's rather than leaving it: the
// target ends up identical to the source. Setting a value the building already holds
// is a no-op in the simulation (REQ-MAT-INPUT-BUFFER), which is what keeps clicking
// an already-matching building free of buffer and production-progress loss.
if (source.type == BuildingType::Splitter)
{
// Operational splitters are configured by tile, sites by BuildingId (mirrors
// SelectedBuildingPanel::onSplitterFilterChanged). Locked item types are dropped
// per REQ-LOCK-UI-BLUEPRINT.
const std::vector<ItemType> filterA = filterUnlockedItems(source.splitterFilterA, *m_sim);
const std::vector<ItemType> filterB = filterUnlockedItems(source.splitterFilterB, *m_sim);
if (const Building* building = findBuilding(m_sim->getFactoryState(), id))
{
std::shared_ptr<SetSplitterFiltersCommand> command =
std::make_shared<SetSplitterFiltersCommand>();
command->tile = building->anchor;
command->filterA = filterA;
command->filterB = filterB;
enqueueCommand(command);
}
else
{
std::shared_ptr<SetSiteSplitterFiltersCommand> command =
std::make_shared<SetSiteSplitterFiltersCommand>();
command->id = id;
command->filterA = filterA;
command->filterB = filterB;
enqueueCommand(command);
}
return;
}
std::shared_ptr<SetRecipeCommand> recipeCommand = std::make_shared<SetRecipeCommand>();
recipeCommand->id = id;
recipeCommand->recipeId = unlockedRecipeId(source);
enqueueCommand(recipeCommand);
// After the schematic, never before: a genuine schematic change resets the layout,
// and both commands drain in order at the next tick boundary.
if (source.type == BuildingType::Shipyard)
{
std::shared_ptr<SetShipLayoutCommand> layoutCommand =
std::make_shared<SetShipLayoutCommand>();
layoutCommand->id = id;
layoutCommand->layout = source.shipLayout.value_or(ShipLayoutConfig{});
enqueueCommand(layoutCommand);
}
}
void GameWorldView::updateTunnelGhost()
{
// The connection preview and entry/exit switch only apply at a valid placement