target single-building transfers by hovering, not by footprint coincidence
This commit is contained in:
@@ -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};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1050,18 +1050,25 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
|
||||
|
||||
namespace
|
||||
{
|
||||
// A single-building blueprint, whose cursor sits on the ghost's own anchor unless a test
|
||||
// says otherwise. That gesture hit-tests the cursor for its transfer target.
|
||||
BlueprintGhostResolved resolveOne(const PlacementFixture& f, BuildingType type,
|
||||
QPoint anchor, Rotation rotation)
|
||||
{
|
||||
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation,
|
||||
/*blueprintHoldsOneBuilding*/ true);
|
||||
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation, anchor);
|
||||
}
|
||||
|
||||
BlueprintGhostResolved resolveOneHovering(const PlacementFixture& f, BuildingType type,
|
||||
QPoint anchor, Rotation rotation, QPoint cursorTile)
|
||||
{
|
||||
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation, cursorTile);
|
||||
}
|
||||
|
||||
// One ghost of a constellation: no cursor hit-test, judged purely on where it sits.
|
||||
BlueprintGhostResolved resolveInConstellation(const PlacementFixture& f, BuildingType type,
|
||||
QPoint anchor, Rotation rotation)
|
||||
{
|
||||
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation,
|
||||
/*blueprintHoldsOneBuilding*/ false);
|
||||
return resolveBlueprintGhost(f.state, f.cfg, type, anchor, rotation, std::nullopt);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -1148,8 +1155,9 @@ TEST_CASE("resolveBlueprintGhost: a single configurable building transfers its s
|
||||
|
||||
TEST_CASE("resolveBlueprintGhost: a transfer ignores the target's rotation", "[blueprint]")
|
||||
{
|
||||
// The rule transfer does not share with compatible overlap: a transfer never rotates
|
||||
// anything, so which way the target faces cannot matter (REQ-UI-BLUEPRINT-TRANSFER).
|
||||
// A transfer never rotates anything, so which way the target faces cannot matter
|
||||
// (REQ-UI-BLUEPRINT-TRANSFER). The ghost snaps to the target's facing rather than
|
||||
// keeping the blueprint's.
|
||||
PlacementFixture f;
|
||||
|
||||
const BuildingId id =
|
||||
@@ -1160,6 +1168,47 @@ TEST_CASE("resolveBlueprintGhost: a transfer ignores the target's rotation", "[b
|
||||
REQUIRE(resolved.action == BlueprintGhostAction::Transfer);
|
||||
REQUIRE(resolved.targetId.has_value());
|
||||
CHECK(*resolved.targetId == id);
|
||||
CHECK(resolved.ghostRotation == Rotation::East);
|
||||
}
|
||||
|
||||
TEST_CASE("resolveBlueprintGhost: a single-building blueprint transfers from anywhere on the target",
|
||||
"[blueprint]")
|
||||
{
|
||||
// The point of hit-testing the cursor instead of comparing footprints. Coincidence
|
||||
// needs the ghost's anchor to land on the target's own anchor, so with a 2x2 body
|
||||
// three of its four tiles missed and read as an ordinary overlap. Hovering any body
|
||||
// tile now targets it, and the ghost snaps onto the building
|
||||
// (REQ-UI-BLUEPRINT-TRANSFER).
|
||||
PlacementFixture f;
|
||||
|
||||
// Assembler body covers (-3,0),(-2,0),(-3,1),(-2,1); its anchor is (-3,0).
|
||||
const BuildingId id =
|
||||
f.bs.place(f.state, BuildingType::Assembler, QPoint(-3, 0), Rotation::East, 0).value();
|
||||
|
||||
const QPoint offAnchorTile(-2, 1);
|
||||
const BlueprintGhostResolved resolved =
|
||||
resolveOneHovering(f, BuildingType::Assembler, offAnchorTile, Rotation::East,
|
||||
offAnchorTile);
|
||||
REQUIRE(resolved.action == BlueprintGhostAction::Transfer);
|
||||
CHECK(*resolved.targetId == id);
|
||||
CHECK(resolved.ghostAnchor == QPoint(-3, 0));
|
||||
|
||||
// The same misaligned ghost inside a constellation still just overlaps invalidly:
|
||||
// a layout is placed where the blueprint puts it, and nothing snaps.
|
||||
CHECK(resolveInConstellation(f, BuildingType::Assembler, offAnchorTile, Rotation::East).action
|
||||
== BlueprintGhostAction::Invalid);
|
||||
}
|
||||
|
||||
TEST_CASE("resolveBlueprintGhost: hovering a different building type does not transfer",
|
||||
"[blueprint]")
|
||||
{
|
||||
PlacementFixture f;
|
||||
|
||||
f.bs.place(f.state, BuildingType::Smelter, QPoint(-3, 0), Rotation::East, 0);
|
||||
|
||||
// A miner blueprint over a smelter: the cursor hit-test only matches its own type.
|
||||
CHECK(resolveOne(f, BuildingType::Miner, QPoint(-3, 0), Rotation::East).action
|
||||
== BlueprintGhostAction::Invalid);
|
||||
}
|
||||
|
||||
TEST_CASE("resolveBlueprintGhost: a construction site is a transfer target too",
|
||||
@@ -1215,29 +1264,18 @@ TEST_CASE("resolveBlueprintGhost: a constellation transfers onto a matching buil
|
||||
CHECK(*resolved.targetId == id);
|
||||
}
|
||||
|
||||
TEST_CASE("resolveBlueprintGhost: only a single-building blueprint ignores target rotation",
|
||||
TEST_CASE("resolveBlueprintGhost: a constellation still requires a matching rotation",
|
||||
"[blueprint]")
|
||||
{
|
||||
// The one thing blueprint size still decides. Inside a constellation a differently
|
||||
// facing twin cannot be re-oriented, so it blocks the whole placement; alone, the
|
||||
// gesture transfers anyway because it rotates nothing (REQ-UI-BLUEPRINT-TRANSFER).
|
||||
//
|
||||
// A splitter, because the question only arises for a footprint that survives
|
||||
// rotation. A miner's body is L-shaped ("AA" / "A>"), so a rotated miner ghost covers
|
||||
// different tiles and coincides with nothing at all -- invalid for a reason that has
|
||||
// nothing to do with these rules.
|
||||
// Only the single-building gesture is forgiving about facing. A constellation's
|
||||
// ghosts stay where the blueprint puts them, and one that cannot be re-oriented to
|
||||
// match blocks the whole placement (REQ-UI-BLUEPRINT-OVERLAP).
|
||||
PlacementFixture f;
|
||||
|
||||
const BuildingId id =
|
||||
f.bs.place(f.state, BuildingType::Splitter, QPoint(-1, 0), Rotation::East, 0).value();
|
||||
f.bs.place(f.state, BuildingType::Splitter, QPoint(-1, 0), Rotation::East, 0);
|
||||
|
||||
CHECK(resolveInConstellation(f, BuildingType::Splitter, QPoint(-1, 0), Rotation::North).action
|
||||
== BlueprintGhostAction::Invalid);
|
||||
|
||||
const BlueprintGhostResolved alone =
|
||||
resolveOne(f, BuildingType::Splitter, QPoint(-1, 0), Rotation::North);
|
||||
REQUIRE(alone.action == BlueprintGhostAction::Transfer);
|
||||
CHECK(*alone.targetId == id);
|
||||
}
|
||||
|
||||
TEST_CASE("resolveBlueprintGhost: a constellation mixes transfers and plain overlaps",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user