store belts as buildings and fix issue that belts could not be selected

This commit is contained in:
2026-04-24 21:09:28 +02:00
parent fff5d43352
commit 55997ef851
4 changed files with 102 additions and 133 deletions

View File

@@ -241,24 +241,6 @@ EntityId BuildingSystem::place(BuildingType type, QPoint anchor,
int BuildingSystem::demolish(EntityId id) int BuildingSystem::demolish(EntityId id)
{ {
// Belt / splitter?
const std::map<EntityId, BeltEntry>::iterator beltIt = m_beltEntities.find(id);
if (beltIt != m_beltEntities.end())
{
const QPoint tile = beltIt->second.tile;
const BuildingType btype = beltIt->second.type;
m_belts.removeTile(tile);
m_tileOccupancy.erase({tile.x(), tile.y()});
m_beltEntities.erase(beltIt);
const BuildingDef* def = findBuildingDef(btype);
if (def)
{
return def->cost * m_config.world.refundPercentage / 100;
}
return 0;
}
// Construction queue? // Construction queue?
for (std::deque<ConstructionSite>::iterator it = m_constructionQueue.begin(); for (std::deque<ConstructionSite>::iterator it = m_constructionQueue.begin();
it != m_constructionQueue.end(); it != m_constructionQueue.end();
@@ -287,6 +269,10 @@ int BuildingSystem::demolish(EntityId id)
{ {
if (it->id == id) if (it->id == id)
{ {
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter)
{
m_belts.removeTile(it->anchor);
}
const BuildingDef* def = findBuildingDef(it->type); const BuildingDef* def = findBuildingDef(it->type);
for (const QPoint& cell : it->bodyCells) for (const QPoint& cell : it->bodyCells)
{ {
@@ -381,71 +367,65 @@ void BuildingSystem::tickConstruction(Tick currentTick)
return; return;
} }
// Promote construction site — belts/splitters go into BeltSystem, others become Buildings. // Promote construction site to an operational Building.
const BuildingDef* def = findBuildingDef(front.type);
const ParsedSurfaceMask mask = parseSurfaceMask(
def ? def->surfaceMask : std::vector<std::string>{},
front.rotation);
Building building;
building.id = front.id;
building.anchor = front.anchor;
building.footprint = front.footprint;
building.rotation = front.rotation;
building.type = front.type;
building.hp = 100.0f;
building.maxHp = 100.0f;
building.recipeId = front.recipeId;
for (const QPoint& cell : mask.bodyCells)
{
building.bodyCells.push_back(front.anchor + cell);
}
for (const Port& port : mask.outputPorts)
{
Port absPort;
absPort.tile = front.anchor + port.tile;
absPort.direction = port.direction;
building.outputPorts.push_back(absPort);
}
building.inputPorts = computeInputPorts(building);
if (!building.recipeId.empty())
{
if (building.type == BuildingType::Shipyard)
{
initShipyardBuffers(building);
}
else
{
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
if (recipe)
{
initBuffers(building, *recipe);
}
}
}
// Register with BeltSystem before the move (mask stays valid).
if (front.type == BuildingType::Belt) if (front.type == BuildingType::Belt)
{ {
m_belts.placeBelt(front.anchor, front.rotation); m_belts.placeBelt(front.anchor, front.rotation);
m_beltEntities[front.id] = BeltEntry{front.anchor, BuildingType::Belt, front.rotation, front.rotation};
} }
else if (front.type == BuildingType::Splitter) else if (front.type == BuildingType::Splitter)
{ {
const BuildingDef* def = findBuildingDef(front.type);
assert(def != nullptr);
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, front.rotation);
assert(mask.outputPorts.size() >= 2); assert(mask.outputPorts.size() >= 2);
const Rotation outA = mask.outputPorts[0].direction; m_belts.placeSplitter(front.anchor,
const Rotation outB = mask.outputPorts[1].direction; mask.outputPorts[0].direction,
m_belts.placeSplitter(front.anchor, outA, outB); mask.outputPorts[1].direction);
m_beltEntities[front.id] = BeltEntry{front.anchor, BuildingType::Splitter, outA, outB};
} }
else
{
const BuildingDef* def = findBuildingDef(front.type);
const ParsedSurfaceMask mask = parseSurfaceMask(
def ? def->surfaceMask : std::vector<std::string>{},
front.rotation);
Building building; m_buildings.push_back(std::move(building));
building.id = front.id;
building.anchor = front.anchor;
building.footprint = front.footprint;
building.rotation = front.rotation;
building.type = front.type;
building.hp = 100.0f;
building.maxHp = 100.0f;
building.recipeId = front.recipeId;
for (const QPoint& cell : mask.bodyCells)
{
building.bodyCells.push_back(front.anchor + cell);
}
for (const Port& port : mask.outputPorts)
{
Port absPort;
absPort.tile = front.anchor + port.tile;
absPort.direction = port.direction;
building.outputPorts.push_back(absPort);
}
building.inputPorts = computeInputPorts(building);
if (!building.recipeId.empty())
{
if (building.type == BuildingType::Shipyard)
{
initShipyardBuffers(building);
}
else
{
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
if (recipe)
{
initBuffers(building, *recipe);
}
}
}
m_buildings.push_back(std::move(building));
}
m_constructionQueue.pop_front(); m_constructionQueue.pop_front();
@@ -774,15 +754,30 @@ std::vector<ConstructionSite> BuildingSystem::allSites() const
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const
{ {
std::vector<BeltTileInfo> result; std::vector<BeltTileInfo> result;
result.reserve(m_beltEntities.size()); for (const Building& b : m_buildings)
for (const std::map<EntityId, BeltEntry>::value_type& kv : m_beltEntities)
{ {
if (b.type != BuildingType::Belt && b.type != BuildingType::Splitter)
{
continue;
}
BeltTileInfo info; BeltTileInfo info;
info.id = kv.first; info.id = b.id;
info.tile = kv.second.tile; info.tile = b.bodyCells.empty() ? b.anchor : b.bodyCells[0];
info.type = kv.second.type; info.type = b.type;
info.directionA = kv.second.directionA; if (!b.outputPorts.empty())
info.directionB = kv.second.directionB; {
info.directionA = b.outputPorts[0].direction;
info.directionB = b.outputPorts[0].direction;
}
else
{
info.directionA = b.rotation;
info.directionB = b.rotation;
}
if (b.type == BuildingType::Splitter && b.outputPorts.size() >= 2)
{
info.directionB = b.outputPorts[1].direction;
}
result.push_back(info); result.push_back(info);
} }
return result; return result;
@@ -907,6 +902,10 @@ bool BuildingSystem::removeBuilding(EntityId id)
{ {
if (it->id == id) if (it->id == id)
{ {
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter)
{
m_belts.removeTile(it->anchor);
}
for (const QPoint& cell : it->bodyCells) for (const QPoint& cell : it->bodyCells)
{ {
m_tileOccupancy.erase({cell.x(), cell.y()}); m_tileOccupancy.erase({cell.x(), cell.y()});

View File

@@ -23,8 +23,8 @@
// Manages building placement, construction queuing, and the per-tick // Manages building placement, construction queuing, and the per-tick
// production loop (belt→building pull, production, building→belt push). // production loop (belt→building pull, production, building→belt push).
// Belt and Splitter types are forwarded to BeltSystem rather than stored // All types including Belt and Splitter are stored as Building instances;
// as Building instances. // BeltSystem owns the per-tile simulation data (item slots, flow).
class BuildingSystem class BuildingSystem
{ {
public: public:
@@ -106,14 +106,6 @@ public:
void forEachBuilding(std::function<void(Building&)> fn); void forEachBuilding(std::function<void(Building&)> fn);
private: private:
struct BeltEntry
{
QPoint tile;
BuildingType type; // Belt or Splitter
Rotation directionA; // Belt: its direction; Splitter: first output
Rotation directionB; // Splitter: second output; Belt: same as directionA
};
const BuildingDef* findBuildingDef(BuildingType type) const; const BuildingDef* findBuildingDef(BuildingType type) const;
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const; const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
const ShipDef* findShipDef(const std::string& id) const; const ShipDef* findShipDef(const std::string& id) const;
@@ -131,7 +123,6 @@ private:
std::vector<Building> m_buildings; std::vector<Building> m_buildings;
std::deque<ConstructionSite> m_constructionQueue; std::deque<ConstructionSite> m_constructionQueue;
std::map<EntityId, BeltEntry> m_beltEntities;
// Maps every occupied body-cell coordinate to the entity that owns it. // Maps every occupied body-cell coordinate to the entity that owns it.
std::map<std::pair<int, int>, EntityId> m_tileOccupancy; std::map<std::pair<int, int>, EntityId> m_tileOccupancy;

View File

@@ -116,7 +116,9 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick); runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"))); REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore")));
REQUIRE(bs.allBuildings().empty()); // belts do not create Building instances REQUIRE(bs.allBuildings().size() == 1);
REQUIRE(bs.allBuildings()[0].type == BuildingType::Belt);
REQUIRE(bs.allBuildings()[0].anchor == QPoint(5, 5));
} }
TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]") TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]")

View File

@@ -396,10 +396,6 @@ EntityId GameWorldView::buildingAtTile(QPoint tile) const
if (cell == tile) { return b.id; } if (cell == tile) { return b.id; }
} }
} }
for (const BuildingSystem::BeltTileInfo& info : m_sim->buildings().allBeltTiles())
{
if (info.tile == tile) { return info.id; }
}
return kInvalidEntityId; return kInvalidEntityId;
} }
@@ -542,10 +538,23 @@ void GameWorldView::drawBuildings(QPainter& painter)
painter.fillRect(tileRect(cell), bv.fill); painter.fillRect(tileRect(cell), bv.fill);
} }
const QPointF tl = tileToWidget(b.anchor); // Belts and splitters are 1×1 on their body cell; other buildings use
const QRectF bboxRect(tl.x(), tl.y(), // the full footprint bounding box for their outline and glyph.
b.footprint.width() * static_cast<qreal>(tilePx()), QRectF bboxRect;
b.footprint.height() * static_cast<qreal>(tilePx())); const bool isBeltLike = (b.type == BuildingType::Belt
|| b.type == BuildingType::Splitter);
if (isBeltLike && !b.bodyCells.empty())
{
const QPointF tl = tileToWidget(b.bodyCells[0]);
bboxRect = QRectF(tl.x(), tl.y(), tilePx(), tilePx());
}
else
{
const QPointF tl = tileToWidget(b.anchor);
bboxRect = QRectF(tl.x(), tl.y(),
b.footprint.width() * static_cast<qreal>(tilePx()),
b.footprint.height() * static_cast<qreal>(tilePx()));
}
painter.setPen(QPen(bv.outline, 1)); painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush); painter.setBrush(Qt::NoBrush);
@@ -576,27 +585,6 @@ void GameWorldView::drawBuildings(QPainter& painter)
} }
} }
// Belt and splitter tiles (stored separately from regular buildings)
for (const BuildingSystem::BeltTileInfo& info : m_sim->buildings().allBeltTiles())
{
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
m_visuals->buildings.find(info.type);
if (it == m_visuals->buildings.end()) { continue; }
const BuildingVisuals& bv = it->second;
painter.setPen(Qt::NoPen);
painter.fillRect(tileRect(info.tile), bv.fill);
painter.setPen(QPen(bv.outline, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRect(tileRect(info.tile));
drawPortGlyph(painter, info.tile, info.directionA, bv.outline);
if (info.type == BuildingType::Splitter)
{
drawPortGlyph(painter, info.tile, info.directionB, bv.outline);
}
}
painter.setOpacity(0.5); painter.setOpacity(0.5);
for (const ConstructionSite& s : m_sim->buildings().allSites()) for (const ConstructionSite& s : m_sim->buildings().allSites())
{ {
@@ -782,17 +770,6 @@ void GameWorldView::drawOverlays(QPainter& painter)
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint); painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
} }
} }
else
{
for (const BuildingSystem::BeltTileInfo& info : m_sim->buildings().allBeltTiles())
{
if (info.id == m_demolishHoverId)
{
painter.fillRect(tileRect(info.tile), m_visuals->overlays.demolishTint);
break;
}
}
}
} }
// Box-select rectangle // Box-select rectangle