Snap belt-drag end tile to a building's input edge
When a belt drag ends with the cursor over a non-belt building or construction site, the end tile now snaps to the input-capable adjacent tile closest to the cursor and points into the target, instead of showing an invalid ghost on the occupied tile. Adds BuildingSystem::getInputPorts(id) (reusing computeInputPorts, refactored into a bodyCells+outputPorts core) as the single source of truth for a target's input-capable adjacent tiles; works for both operational buildings (stored inputPorts) and construction sites (mask-derived). GameWorldView tracks the cursor world position and, in recomputeBeltDragPath, picks the nearest input port and overrides the end tile's rotation. Ghost preview, resolve, and apply consume the snapped path unchanged. Implements REQ-BLD-BELT-DRAG snapping-to-a-building. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -267,16 +267,23 @@ void BuildingSystem::initSalvageBayBuffer(Building& b) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
||||||
|
{
|
||||||
|
return computeInputPorts(b.bodyCells, b.outputPorts);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Port> BuildingSystem::computeInputPorts(
|
||||||
|
const std::vector<QPoint>& bodyCells,
|
||||||
|
const std::vector<Port>& outputPorts) const
|
||||||
{
|
{
|
||||||
// Build lookup sets for quick membership checks.
|
// Build lookup sets for quick membership checks.
|
||||||
std::set<std::pair<int, int>> bodySet;
|
std::set<std::pair<int, int>> bodySet;
|
||||||
for (const QPoint& cell : b.bodyCells)
|
for (const QPoint& cell : bodyCells)
|
||||||
{
|
{
|
||||||
bodySet.insert({cell.x(), cell.y()});
|
bodySet.insert({cell.x(), cell.y()});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::set<std::pair<int, int>> outputPortTiles;
|
std::set<std::pair<int, int>> outputPortTiles;
|
||||||
for (const Port& port : b.outputPorts)
|
for (const Port& port : outputPorts)
|
||||||
{
|
{
|
||||||
outputPortTiles.insert({port.tile.x(), port.tile.y()});
|
outputPortTiles.insert({port.tile.x(), port.tile.y()});
|
||||||
}
|
}
|
||||||
@@ -294,7 +301,7 @@ std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
|||||||
std::set<std::pair<int, int>> seen;
|
std::set<std::pair<int, int>> seen;
|
||||||
std::vector<Port> inputPorts;
|
std::vector<Port> inputPorts;
|
||||||
|
|
||||||
for (const QPoint& cell : b.bodyCells)
|
for (const QPoint& cell : bodyCells)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 4; ++i)
|
for (int i = 0; i < 4; ++i)
|
||||||
{
|
{
|
||||||
@@ -317,6 +324,30 @@ std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
|||||||
return inputPorts;
|
return inputPorts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<Port> BuildingSystem::getInputPorts(BuildingId id) const
|
||||||
|
{
|
||||||
|
if (const Building* building = findBuilding(id))
|
||||||
|
{
|
||||||
|
return building->inputPorts;
|
||||||
|
}
|
||||||
|
if (const ConstructionSite* site = findSite(id))
|
||||||
|
{
|
||||||
|
// A site stores no ports; derive its output ports from the mask (absolute)
|
||||||
|
// and run the same input-edge scan (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS).
|
||||||
|
const BuildingDef* def = findBuildingDef(site->type);
|
||||||
|
if (def == nullptr) { return {}; }
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, site->rotation);
|
||||||
|
std::vector<Port> outputPortsAbsolute;
|
||||||
|
outputPortsAbsolute.reserve(mask.outputPorts.size());
|
||||||
|
for (const Port& port : mask.outputPorts)
|
||||||
|
{
|
||||||
|
outputPortsAbsolute.push_back(Port{ site->anchor + port.tile, port.direction });
|
||||||
|
}
|
||||||
|
return computeInputPorts(site->bodyCells, outputPortsAbsolute);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
|
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
|
||||||
{
|
{
|
||||||
std::vector<const RecipeOutput*> eligible;
|
std::vector<const RecipeOutput*> eligible;
|
||||||
|
|||||||
@@ -171,6 +171,12 @@ public:
|
|||||||
// Find nearest operational building of the given type; nullptr if none.
|
// Find nearest operational building of the given type; nullptr if none.
|
||||||
const Building* findNearestBuilding(QVector2D worldPos, BuildingType type) const;
|
const Building* findNearestBuilding(QVector2D worldPos, BuildingType type) const;
|
||||||
|
|
||||||
|
// Input-capable adjacent tiles for a building or construction site
|
||||||
|
// (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS): each returned Port.tile is the
|
||||||
|
// outside adjacent tile and Port.direction is the belt facing that points into
|
||||||
|
// the target. Output-port edges are excluded. Empty for an unknown id.
|
||||||
|
std::vector<Port> getInputPorts(BuildingId id) const;
|
||||||
|
|
||||||
// Register / unregister tile occupancy for ECS station entities.
|
// Register / unregister tile occupancy for ECS station entities.
|
||||||
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
||||||
void unregisterTileOccupancy(const std::vector<QPoint>& cells);
|
void unregisterTileOccupancy(const std::vector<QPoint>& cells);
|
||||||
@@ -246,6 +252,9 @@ private:
|
|||||||
void initShipyardBuffers(Building& b) const;
|
void initShipyardBuffers(Building& b) const;
|
||||||
void initSalvageBayBuffer(Building& b) const;
|
void initSalvageBayBuffer(Building& b) const;
|
||||||
std::vector<Port> computeInputPorts(const Building& b) const;
|
std::vector<Port> computeInputPorts(const Building& b) const;
|
||||||
|
// Core input-edge scan shared by operational buildings and construction sites.
|
||||||
|
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
|
||||||
|
const std::vector<Port>& outputPorts) const;
|
||||||
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
||||||
bool bodyCellsWithinWorldBounds(
|
bool bodyCellsWithinWorldBounds(
|
||||||
const std::vector<QPoint>& bodyCells,
|
const std::vector<QPoint>& bodyCells,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
@@ -1421,3 +1423,106 @@ TEST_CASE("BuildingSystem: getProductionStatus classifies production state", "[b
|
|||||||
REQUIRE(statusOf(bay) == ProductionStatus::Producing); // holding scrap -> green
|
REQUIRE(statusOf(bay) == ProductionStatus::Producing); // holding scrap -> green
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// getInputPorts (REQ-BLD-BELT-DRAG snapping, REQ-MAT-INPUT-PORTS)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
QPoint directionDelta(Rotation direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case Rotation::North: return QPoint(0, -1);
|
||||||
|
case Rotation::East: return QPoint(1, 0);
|
||||||
|
case Rotation::South: return QPoint(0, 1);
|
||||||
|
case Rotation::West: return QPoint(-1, 0);
|
||||||
|
}
|
||||||
|
return QPoint(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasInputPort(const std::vector<Port>& ports, QPoint tile, Rotation direction)
|
||||||
|
{
|
||||||
|
for (const Port& port : ports)
|
||||||
|
{
|
||||||
|
if (port.tile == tile && port.direction == direction) { return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advances the sim until the given site becomes an operational building, or a
|
||||||
|
// safety cap is reached.
|
||||||
|
void buildToCompletion(BuildingSystem& bs, BeltSystem& belts, BuildingId id,
|
||||||
|
Tick& tick)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 20000 && bs.findBuilding(id) == nullptr; ++i)
|
||||||
|
{
|
||||||
|
runTicks(bs, belts, 1, tick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: getInputPorts on a miner site lists every input edge", "[building]")
|
||||||
|
{
|
||||||
|
PlacementFixture f;
|
||||||
|
// Miner mask ["AA","A>"] East → body (0,0),(1,0),(0,1); output tile (1,1) East.
|
||||||
|
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
|
const std::vector<Port> ports = f.bs.getInputPorts(id);
|
||||||
|
|
||||||
|
// Every perimeter edge except the output-port edge at (1,1), each pointing in.
|
||||||
|
REQUIRE(ports.size() == 6);
|
||||||
|
REQUIRE(hasInputPort(ports, QPoint(-1, 0), Rotation::East));
|
||||||
|
REQUIRE(hasInputPort(ports, QPoint(0, -1), Rotation::South));
|
||||||
|
REQUIRE(hasInputPort(ports, QPoint(2, 0), Rotation::West));
|
||||||
|
REQUIRE(hasInputPort(ports, QPoint(1, -1), Rotation::South));
|
||||||
|
REQUIRE(hasInputPort(ports, QPoint(-1, 1), Rotation::East));
|
||||||
|
REQUIRE(hasInputPort(ports, QPoint(0, 2), Rotation::North));
|
||||||
|
|
||||||
|
// The output-port tile is never an input port.
|
||||||
|
REQUIRE_FALSE(hasInputPort(ports, QPoint(1, 1), Rotation::North));
|
||||||
|
REQUIRE_FALSE(hasInputPort(ports, QPoint(1, 1), Rotation::West));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built building",
|
||||||
|
"[building]")
|
||||||
|
{
|
||||||
|
PlacementFixture f;
|
||||||
|
Tick tick = 0;
|
||||||
|
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||||
|
|
||||||
|
const std::vector<Port> sitePorts = f.bs.getInputPorts(id);
|
||||||
|
buildToCompletion(f.bs, f.belts, id, tick);
|
||||||
|
REQUIRE(f.bs.findBuilding(id) != nullptr);
|
||||||
|
const std::vector<Port> builtPorts = f.bs.getInputPorts(id);
|
||||||
|
|
||||||
|
// The operational path (stored inputPorts) agrees with the site path (mask-derived).
|
||||||
|
REQUIRE(builtPorts.size() == sitePorts.size());
|
||||||
|
for (const Port& port : sitePorts)
|
||||||
|
{
|
||||||
|
REQUIRE(hasInputPort(builtPorts, port.tile, port.direction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: getInputPorts invariants hold for a rotated site", "[building]")
|
||||||
|
{
|
||||||
|
PlacementFixture f;
|
||||||
|
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::South, 0).value();
|
||||||
|
const ConstructionSite* site = f.bs.findSite(id);
|
||||||
|
REQUIRE(site != nullptr);
|
||||||
|
|
||||||
|
std::set<std::pair<int, int>> bodySet;
|
||||||
|
for (const QPoint& cell : site->bodyCells) { bodySet.insert({cell.x(), cell.y()}); }
|
||||||
|
|
||||||
|
const std::vector<Port> ports = f.bs.getInputPorts(id);
|
||||||
|
REQUIRE_FALSE(ports.empty());
|
||||||
|
for (const Port& port : ports)
|
||||||
|
{
|
||||||
|
// Each port tile is outside the footprint...
|
||||||
|
REQUIRE(bodySet.count({port.tile.x(), port.tile.y()}) == 0);
|
||||||
|
// ...and its direction points into an adjacent body cell.
|
||||||
|
const QPoint into = port.tile + directionDelta(port.direction);
|
||||||
|
REQUIRE(bodySet.count({into.x(), into.y()}) == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -937,7 +937,61 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
|
|
||||||
void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
|
void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
|
||||||
{
|
{
|
||||||
m_beltDragPath = computeBeltDragPath(m_beltDragAnchor, cursorTile, m_ghostRotation);
|
QPoint endTile = cursorTile;
|
||||||
|
std::optional<Rotation> forcedEndRotation;
|
||||||
|
|
||||||
|
// If the cursor is over a non-belt building or construction site, snap the end
|
||||||
|
// tile to the input-capable adjacent tile closest to the cursor, pointing into
|
||||||
|
// the target (REQ-BLD-BELT-DRAG).
|
||||||
|
std::optional<BuildingId> targetId = buildingAtTile(cursorTile);
|
||||||
|
std::optional<BuildingType> targetType;
|
||||||
|
if (targetId.has_value())
|
||||||
|
{
|
||||||
|
if (const Building* building = m_sim->getBuildings().findBuilding(*targetId))
|
||||||
|
{
|
||||||
|
targetType = building->type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (std::optional<BuildingId> siteId = siteAtTile(cursorTile); siteId.has_value())
|
||||||
|
{
|
||||||
|
targetId = siteId;
|
||||||
|
if (const ConstructionSite* site = m_sim->getBuildings().findSite(*siteId))
|
||||||
|
{
|
||||||
|
targetType = site->type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetId.has_value() && targetType.has_value()
|
||||||
|
&& *targetType != BuildingType::Belt)
|
||||||
|
{
|
||||||
|
const std::vector<Port> inputPorts = m_sim->getBuildings().getInputPorts(*targetId);
|
||||||
|
std::optional<Port> best;
|
||||||
|
float bestDistanceSq = 0.0f;
|
||||||
|
for (const Port& port : inputPorts)
|
||||||
|
{
|
||||||
|
const QVector2D center(static_cast<float>(port.tile.x()) + 0.5f,
|
||||||
|
static_cast<float>(port.tile.y()) + 0.5f);
|
||||||
|
const float distanceSq = (center - m_cursorWorldPos).lengthSquared();
|
||||||
|
if (!best.has_value() || distanceSq < bestDistanceSq)
|
||||||
|
{
|
||||||
|
best = port;
|
||||||
|
bestDistanceSq = distanceSq;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best.has_value())
|
||||||
|
{
|
||||||
|
endTile = best->tile;
|
||||||
|
forcedEndRotation = best->direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_beltDragPath = computeBeltDragPath(m_beltDragAnchor, endTile, m_ghostRotation);
|
||||||
|
if (forcedEndRotation.has_value() && !m_beltDragPath.empty())
|
||||||
|
{
|
||||||
|
// The end tile points into the target, overriding its incoming-step
|
||||||
|
// orientation (REQ-BLD-BELT-DRAG "Snapping to a building").
|
||||||
|
m_beltDragPath.back().rotation = *forcedEndRotation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath() const
|
std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath() const
|
||||||
@@ -2187,6 +2241,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
// is placed until release (REQ-BLD-BELT-DRAG).
|
// is placed until release (REQ-BLD-BELT-DRAG).
|
||||||
m_dragging = true;
|
m_dragging = true;
|
||||||
m_beltDragAnchor = tile;
|
m_beltDragAnchor = tile;
|
||||||
|
m_cursorWorldPos = widgetToWorld(event->pos());
|
||||||
recomputeBeltDragPath(tile);
|
recomputeBeltDragPath(tile);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2350,6 +2405,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||||
{
|
{
|
||||||
const QPoint tile = widgetToTile(event->pos());
|
const QPoint tile = widgetToTile(event->pos());
|
||||||
|
m_cursorWorldPos = widgetToWorld(event->pos());
|
||||||
|
|
||||||
if (m_builderType.has_value())
|
if (m_builderType.has_value())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -279,6 +279,9 @@ private:
|
|||||||
// on release. Empty unless a belt drag is in progress.
|
// on release. Empty unless a belt drag is in progress.
|
||||||
std::vector<BeltPathTile> m_beltDragPath;
|
std::vector<BeltPathTile> m_beltDragPath;
|
||||||
QPoint m_beltDragAnchor;
|
QPoint m_beltDragAnchor;
|
||||||
|
// Last known cursor position in world (tile) units; used to pick the belt-drag
|
||||||
|
// end tile closest to the cursor when snapping to a building (REQ-BLD-BELT-DRAG).
|
||||||
|
QVector2D m_cursorWorldPos;
|
||||||
bool m_dragging;
|
bool m_dragging;
|
||||||
|
|
||||||
std::optional<Blueprint> m_blueprintMode;
|
std::optional<Blueprint> m_blueprintMode;
|
||||||
|
|||||||
Reference in New Issue
Block a user