target single-building transfers by hovering, not by footprint coincidence

This commit is contained in:
2026-08-06 20:31:51 +02:00
parent 98deab932a
commit 3b37b0ecf8
6 changed files with 171 additions and 68 deletions

View File

@@ -632,12 +632,16 @@ 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).
// A single-building blueprint hit-tests the cursor for its transfer target; a
// constellation does not (REQ-UI-BLUEPRINT-TRANSFER). `center` is the cursor tile.
// The size is read from the blueprint as stored, before locked types are dropped, so
// the gesture behaves the same however much the player has unlocked.
const std::optional<QPoint> hoverTile =
m_buildMode.getBlueprint().buildings.size() == 1 ? std::make_optional(center)
: std::nullopt;
return resolveBlueprintGhost(m_sim->getFactoryState(), m_sim->getConfig(),
building.type, center + building.offset, building.rotation,
m_buildMode.getBlueprint().buildings.size() == 1);
hoverTile);
}
std::string GameWorldView::unlockedRecipeId(const BlueprintBuilding& building) const

View File

@@ -970,23 +970,27 @@ 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;
// A single-building blueprint hit-tests the cursor for its transfer target; a
// constellation does not (REQ-UI-BLUEPRINT-TRANSFER). The stored building count,
// not the count after locked types are dropped, so the rule does not shift as the
// player unlocks things.
const QPoint cursorTile = frame.buildMode.getBlueprintGhostTile();
const std::optional<QPoint> hoverTile =
frame.buildMode.getBlueprint().buildings.size() == 1
? std::make_optional(cursorTile) : std::nullopt;
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;
// 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.
// compatible overlap is an ordinary valid ghost. The resolved anchor and
// rotation are drawn rather than the blueprint's own, so a hovered transfer
// target shows the ghost snapped onto it.
const BlueprintGhostResolved resolved = resolveBlueprintGhost(
m_sim.getFactoryState(), m_sim.getConfig(), bb.type, anchor, bb.rotation,
holdsOneBuilding);
m_sim.getFactoryState(), m_sim.getConfig(), bb.type, cursorTile + bb.offset,
bb.rotation, hoverTile);
GhostTint tint = GhostTint::Normal;
if (resolved.action == BlueprintGhostAction::Transfer)
{
@@ -996,8 +1000,9 @@ void WorldRenderer::drawOverlays(QPainter& painter, const WorldCoordinates& coor
{
tint = GhostTint::Invalid;
}
drawBuildingGhost(painter, coordinates, bb.type, anchor, bb.rotation,
tint, /*showPortTargetGlyphs*/ false);
drawBuildingGhost(painter, coordinates, bb.type, resolved.ghostAnchor,
resolved.ghostRotation, tint,
/*showPortTargetGlyphs*/ false);
}
}