Snap belt-drag end tile to a building's input edge

This commit is contained in:
2026-07-21 21:09:07 +02:00
parent 0c4eb480be
commit 4ca5b332cd
6 changed files with 210 additions and 5 deletions

View File

@@ -267,16 +267,23 @@ void BuildingSystem::initSalvageBayBuffer(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.
std::set<std::pair<int, int>> bodySet;
for (const QPoint& cell : b.bodyCells)
for (const QPoint& cell : bodyCells)
{
bodySet.insert({cell.x(), cell.y()});
}
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()});
}
@@ -294,7 +301,7 @@ std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
std::set<std::pair<int, int>> seen;
std::vector<Port> inputPorts;
for (const QPoint& cell : b.bodyCells)
for (const QPoint& cell : bodyCells)
{
for (int i = 0; i < 4; ++i)
{
@@ -317,6 +324,30 @@ std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
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<const RecipeOutput*> eligible;

View File

@@ -171,6 +171,12 @@ public:
// Find nearest operational building of the given type; nullptr if none.
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.
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
void unregisterTileOccupancy(const std::vector<QPoint>& cells);
@@ -246,6 +252,9 @@ private:
void initShipyardBuffers(Building& b) const;
void initSalvageBayBuffer(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);
bool bodyCellsWithinWorldBounds(
const std::vector<QPoint>& bodyCells,

View File

@@ -2,7 +2,9 @@
#include <map>
#include <random>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <QPoint>
@@ -1421,3 +1423,106 @@ TEST_CASE("BuildingSystem: getProductionStatus classifies production state", "[b
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);
}
}

View File

@@ -937,7 +937,61 @@ void GameWorldView::placeAtTile(QPoint tile)
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
@@ -2187,6 +2241,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
// is placed until release (REQ-BLD-BELT-DRAG).
m_dragging = true;
m_beltDragAnchor = tile;
m_cursorWorldPos = widgetToWorld(event->pos());
recomputeBeltDragPath(tile);
}
else
@@ -2350,6 +2405,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
{
const QPoint tile = widgetToTile(event->pos());
m_cursorWorldPos = widgetToWorld(event->pos());
if (m_builderType.has_value())
{

View File

@@ -279,6 +279,9 @@ private:
// on release. Empty unless a belt drag is in progress.
std::vector<BeltPathTile> m_beltDragPath;
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;
std::optional<Blueprint> m_blueprintMode;