Snap belt-drag end tile to a building's input edge
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user