re-cover copy-settings through single-building blueprints
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user