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

@@ -130,22 +130,67 @@ std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state, con
}
BlueprintGhostResolved resolveBlueprintGhost(const FactoryState& state, const GameConfig& config,
BuildingType type, QPoint anchor, Rotation rotation, bool blueprintHoldsOneBuilding)
namespace
{
// Terrain and world bounds first: nothing rescues a ghost hanging off the asteroid
// The building or construction site occupying `tile`, if it is of `type`, together with
// where it stands. The single-building transfer gesture hit-tests the cursor with this
// instead of comparing footprints (REQ-UI-BLUEPRINT-TRANSFER).
std::optional<CoincidingBuilding> findSameTypeBuildingAt(const FactoryState& state,
BuildingType type, QPoint tile,
QPoint& anchorOut)
{
const std::optional<BuildingId> owner = state.grid.findOwner(tile);
if (!owner.has_value()) { return std::nullopt; }
if (const ConstructionSite* site = findSite(state, *owner))
{
if (site->type != type) { return std::nullopt; }
anchorOut = site->anchor;
return CoincidingBuilding{site->id, site->rotation};
}
if (const Building* building = findBuilding(state, *owner))
{
if (building->type != type) { return std::nullopt; }
anchorOut = building->anchor;
return CoincidingBuilding{building->id, building->rotation};
}
return std::nullopt;
}
} // namespace
BlueprintGhostResolved resolveBlueprintGhost(const FactoryState& state, const GameConfig& config,
BuildingType type, QPoint anchor, Rotation rotation, std::optional<QPoint> hoverTile)
{
// The single-building gesture first, because it answers without looking at the
// ghost's own position at all: whatever same-type building the cursor is on takes the
// settings, at any facing, and the ghost snaps onto it (REQ-UI-BLUEPRINT-TRANSFER).
if (hoverTile.has_value() && isConfigurableBuildingType(type))
{
QPoint hoveredAnchor;
const std::optional<CoincidingBuilding> hovered =
findSameTypeBuildingAt(state, type, *hoverTile, hoveredAnchor);
if (hovered.has_value())
{
return BlueprintGhostResolved{BlueprintGhostAction::Transfer, hovered->id,
hoveredAnchor, hovered->rotation};
}
}
// Terrain and world bounds next: nothing rescues a ghost hanging off the asteroid
// (REQ-BLD-PLACE-VALID condition (a)).
if (!isPlacementValid(state, config, type, anchor, rotation))
{
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt};
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt,
anchor, rotation};
}
const std::optional<CoincidingBuilding> coinciding =
findCoincidingSameTypeBuilding(state, config, type, anchor, rotation);
if (coinciding.has_value())
{
const bool configurable = isConfigurableBuildingType(type);
// The building the blueprint wants is already there, facing the same way. It
// takes the blueprint's settings if it has any to take (REQ-UI-BLUEPRINT-TRANSFER)
// and is otherwise left exactly as it is (REQ-UI-BLUEPRINT-OVERLAP). Tunnels are
@@ -153,36 +198,36 @@ BlueprintGhostResolved resolveBlueprintGhost(const FactoryState& state, const Ga
// REQ-BLD-ROTATE-IN-PLACE exception does not arise.
if (coinciding->rotation == rotation)
{
return BlueprintGhostResolved{configurable
return BlueprintGhostResolved{isConfigurableBuildingType(type)
? BlueprintGhostAction::Transfer
: BlueprintGhostAction::CompatibleOverlap,
coinciding->id};
coinciding->id, anchor, rotation};
}
// Facing the other way. Placement may not re-orient it, so this is invalid --
// except for the deliberate one-click gesture of a single-building blueprint,
// where a transfer rotates nothing anyway and the target's facing is beside the
// point (REQ-UI-BLUEPRINT-TRANSFER).
if (blueprintHoldsOneBuilding && configurable)
{
return BlueprintGhostResolved{BlueprintGhostAction::Transfer, coinciding->id};
}
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt};
// Facing the other way, and placement may not re-orient it.
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt,
anchor, rotation};
}
// No coincidence: any occupancy at all is an ordinary overlap
// (REQ-BLD-PLACE-VALID condition (b)).
const BuildingDef* def = config.buildings.findBuildingDef(type);
if (!def) { return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt}; }
if (!def)
{
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt,
anchor, rotation};
}
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rotation);
for (const QPoint& relativeCell : parsed.bodyCells)
{
if (isTileOccupied(state, anchor + relativeCell))
{
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt};
return BlueprintGhostResolved{BlueprintGhostAction::Invalid, std::nullopt,
anchor, rotation};
}
}
return BlueprintGhostResolved{BlueprintGhostAction::PlaceNew, std::nullopt};
return BlueprintGhostResolved{BlueprintGhostAction::PlaceNew, std::nullopt,
anchor, rotation};
}

View File

@@ -79,14 +79,24 @@ struct BlueprintGhostResolved
{
BlueprintGhostAction action;
std::optional<BuildingId> targetId; // set for CompatibleOverlap and Transfer
// Where the ghost belongs on screen. The queried anchor and rotation, except at a
// hovered transfer target, where the ghost snaps onto the target so it shows what the
// click will act on (REQ-UI-BLUEPRINT-TRANSFER).
QPoint ghostAnchor;
Rotation ghostRotation;
};
// Classifies one ghost of a blueprint against the current factory state
// (REQ-UI-BLUEPRINT-OVERLAP, REQ-UI-BLUEPRINT-TRANSFER). `blueprintHoldsOneBuilding`
// only widens the transfer rule to differently-facing targets; a same-facing target
// transfers whatever the blueprint's size. It is the blueprint's stored size, counted
// before locked types are dropped, so the gesture does not change behavior as the player
// unlocks things.
// (REQ-UI-BLUEPRINT-OVERLAP, REQ-UI-BLUEPRINT-TRANSFER).
//
// `hoverTile` is set only for a blueprint holding exactly one building, and is then the
// tile under the cursor. That gesture is a copying tool rather than a layout, so it finds
// its transfer target by hit-testing the cursor instead of by footprint coincidence --
// without which a Shipyard could never be targeted at a different facing, its 4x2
// footprint covering entirely different tiles once rotated. Pass nullopt for a
// constellation, whose ghosts are judged purely by where the blueprint puts them. The
// size is read from the blueprint as stored, before locked types are dropped, so the
// gesture does not change behavior as the player unlocks things.
//
// Shared by the ghost coloring and the click path so a preview cannot disagree with what
// the click then does -- the same reason resolveBeltDragPath is shared.
@@ -94,7 +104,7 @@ BlueprintGhostResolved resolveBlueprintGhost(const FactoryState& state,
const GameConfig& config,
BuildingType type, QPoint anchor,
Rotation rotation,
bool blueprintHoldsOneBuilding);
std::optional<QPoint> hoverTile);
// What a belt drag would do to one tile of its path (REQ-BLD-BELT-DRAG).
enum class BeltTileAction