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
|
||||
|
||||
@@ -187,6 +187,16 @@ private:
|
||||
std::optional<BuildingId> siteAtTile(QPoint tile) const;
|
||||
|
||||
void placeBlueprintAtTile(QPoint center);
|
||||
// resolveBlueprintGhost (PlacementRules) for one building of the blueprint currently
|
||||
// in placement mode, anchored at the cursor tile `center`.
|
||||
BlueprintGhostResolved resolveBlueprintGhostHere(const BlueprintBuilding& building,
|
||||
QPoint center) const;
|
||||
// The blueprint's stored recipe or schematic id, or empty when it stores none or the
|
||||
// stored one is currently locked (REQ-LOCK-UI-BLUEPRINT).
|
||||
std::string unlockedRecipeId(const BlueprintBuilding& building) const;
|
||||
// Applies a single-building blueprint's settings to the building it is hovering,
|
||||
// instead of placing anything (REQ-UI-BLUEPRINT-TRANSFER).
|
||||
void transferConfigTo(BuildingId id, const BlueprintBuilding& source);
|
||||
|
||||
// Drops despawned or fully-collected debris from the selection
|
||||
// (REQ-UI-DEBRIS-CLICK-SELECT). Called each frame from onFrame().
|
||||
|
||||
@@ -48,6 +48,7 @@ struct OverlayVisuals
|
||||
QColor selectionRect;
|
||||
QColor tileHighlight;
|
||||
QColor selectedOutline;
|
||||
QColor configTransfer; // blueprint ghost over a transfer target (REQ-UI-BLUEPRINT-TRANSFER)
|
||||
QColor lockedAsteroid;
|
||||
QColor modalDim;
|
||||
QColor tunnelPreview; // tunnel connection preview highlight (REQ-BLD-TUNNEL-MODE)
|
||||
|
||||
@@ -224,6 +224,7 @@ 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.configTransfer = parseColor(requireString(ov, "config_transfer", "overlays"), "overlays.config_transfer");
|
||||
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");
|
||||
|
||||
@@ -928,7 +928,8 @@ void WorldRenderer::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
||||
const BeltPathTile& entry = frame.buildMode.getBeltDragPath()[index];
|
||||
drawBuildingGhost(painter, coordinates, BuildingType::Belt,
|
||||
entry.tile, entry.rotation,
|
||||
/*valid*/ item.action != BeltTileAction::Invalid,
|
||||
item.action != BeltTileAction::Invalid
|
||||
? GhostTint::Normal : GhostTint::Invalid,
|
||||
/*showPortTargetGlyphs*/ true);
|
||||
}
|
||||
}
|
||||
@@ -960,7 +961,8 @@ void WorldRenderer::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
||||
drawBuildingGhost(painter, coordinates,
|
||||
frame.buildMode.getEffectiveBuilderType(),
|
||||
ghostTile, frame.buildMode.getGhostRotation(),
|
||||
frame.buildMode.isGhostValid(),
|
||||
frame.buildMode.isGhostValid()
|
||||
? GhostTint::Normal : GhostTint::Invalid,
|
||||
/*showPortTargetGlyphs*/ true);
|
||||
}
|
||||
}
|
||||
@@ -968,15 +970,34 @@ void WorldRenderer::drawOverlays(QPainter& painter, const WorldCoordinates& coor
|
||||
// Blueprint placement ghost
|
||||
if (frame.buildMode.isBlueprintMode())
|
||||
{
|
||||
// The stored building count, not the count after locked types are dropped, so the
|
||||
// transfer rule does not shift as the player unlocks things
|
||||
// (REQ-UI-BLUEPRINT-TRANSFER).
|
||||
const bool holdsOneBuilding =
|
||||
frame.buildMode.getBlueprint().buildings.size() == 1;
|
||||
for (const BlueprintBuilding& bb : frame.buildMode.getBlueprint().buildings)
|
||||
{
|
||||
// Locked building types are omitted from the blueprint (REQ-LOCK-BUILDING,
|
||||
// REQ-LOCK-UI-BLUEPRINT), so they are not ghosted either.
|
||||
if (!m_sim.isBuildingUnlocked(bb.type)) { continue; }
|
||||
const QPoint anchor = frame.buildMode.getBlueprintGhostTile() + bb.offset;
|
||||
const bool valid = canPlaceBuilding(m_sim.getFactoryState(), m_sim.getConfig(), bb.type, anchor, bb.rotation);
|
||||
// The same classifier the click path uses, so the color always predicts what
|
||||
// clicking would do (REQ-UI-BLUEPRINT-OVERLAP, REQ-UI-BLUEPRINT-TRANSFER). A
|
||||
// compatible overlap is an ordinary valid ghost.
|
||||
const BlueprintGhostResolved resolved = resolveBlueprintGhost(
|
||||
m_sim.getFactoryState(), m_sim.getConfig(), bb.type, anchor, bb.rotation,
|
||||
holdsOneBuilding);
|
||||
GhostTint tint = GhostTint::Normal;
|
||||
if (resolved.action == BlueprintGhostAction::Transfer)
|
||||
{
|
||||
tint = GhostTint::Transfer;
|
||||
}
|
||||
else if (resolved.action == BlueprintGhostAction::Invalid)
|
||||
{
|
||||
tint = GhostTint::Invalid;
|
||||
}
|
||||
drawBuildingGhost(painter, coordinates, bb.type, anchor, bb.rotation,
|
||||
valid, /*showPortTargetGlyphs*/ false);
|
||||
tint, /*showPortTargetGlyphs*/ false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1048,7 +1069,7 @@ void WorldRenderer::drawBuildingGhost(QPainter& painter,
|
||||
const WorldCoordinates& coordinates,
|
||||
BuildingType type,
|
||||
QPoint anchorTile, Rotation rotation,
|
||||
bool valid, bool showPortTargetGlyphs)
|
||||
GhostTint tint, bool showPortTargetGlyphs)
|
||||
{
|
||||
const BuildingDef* def = m_sim.getConfig().buildings.findBuildingDef(type);
|
||||
if (!def) { return; }
|
||||
@@ -1058,15 +1079,18 @@ void WorldRenderer::drawBuildingGhost(QPainter& painter,
|
||||
if (it == m_visuals.buildings.end()) { return; }
|
||||
const BuildingVisuals& bv = it->second;
|
||||
|
||||
// Valid ghosts show the building type's own colors; invalid ghosts override
|
||||
// with the distinct invalid color (REQ-BLD-GHOST, REQ-BLD-PLACE-VALID). The
|
||||
// invalid color's RGB is taken at full opacity so it does not double-dim
|
||||
// against the setOpacity below (the configured color carries its own alpha).
|
||||
const QColor invalidColor(m_visuals.overlays.ghostInvalid.red(),
|
||||
m_visuals.overlays.ghostInvalid.green(),
|
||||
m_visuals.overlays.ghostInvalid.blue());
|
||||
const QColor fillColor = valid ? bv.fill : invalidColor;
|
||||
const QColor lineColor = valid ? bv.outline : invalidColor;
|
||||
// Normal ghosts show the building type's own colors; the other two tints override
|
||||
// them with a single flat color -- invalid (REQ-BLD-GHOST, REQ-BLD-PLACE-VALID) or
|
||||
// configuration transfer (REQ-UI-BLUEPRINT-TRANSFER). An override's RGB is taken at
|
||||
// full opacity so it does not double-dim against the setOpacity below (the
|
||||
// configured color carries its own alpha).
|
||||
const QColor& configured = tint == GhostTint::Transfer
|
||||
? m_visuals.overlays.configTransfer
|
||||
: m_visuals.overlays.ghostInvalid;
|
||||
const QColor overrideColor(configured.red(), configured.green(), configured.blue());
|
||||
const bool useOwnColors = tint == GhostTint::Normal;
|
||||
const QColor fillColor = useOwnColors ? bv.fill : overrideColor;
|
||||
const QColor lineColor = useOwnColors ? bv.outline : overrideColor;
|
||||
|
||||
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rotation);
|
||||
if (parsed.bodyCells.empty()) { return; }
|
||||
|
||||
@@ -35,6 +35,16 @@ struct ActiveBeam
|
||||
QVector2D targetOffset;
|
||||
};
|
||||
|
||||
// How a placement ghost is colored. Normal shows the building type's own colors
|
||||
// (REQ-BLD-GHOST); the other two override them (REQ-BLD-PLACE-VALID,
|
||||
// REQ-UI-BLUEPRINT-TRANSFER).
|
||||
enum class GhostTint
|
||||
{
|
||||
Normal,
|
||||
Invalid,
|
||||
Transfer
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -113,7 +123,7 @@ private:
|
||||
bool centered);
|
||||
void drawBuildingGhost(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
BuildingType type, QPoint anchorTile, Rotation rotation,
|
||||
bool valid, bool showPortTargetGlyphs);
|
||||
GhostTint tint, bool showPortTargetGlyphs);
|
||||
|
||||
// Loads the per-building world icons (REQ-UI-WORLD-ICON) from
|
||||
// <configDir>/../icons/buildings once at construction. Only the building
|
||||
|
||||
Reference in New Issue
Block a user