#include "PlacementRules.h" #include "BuildingType.h" #include "FactoryQueries.h" #include "SurfaceMask.h" bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,const std::vector& bodyCells, QPoint anchor) { const int heightTiles = config.world.heightTiles; const int leftEdgeX = -state.asteroidWidth_tiles; for (const QPoint& cell : bodyCells) { const QPoint worldCell = anchor + cell; if (worldCell.y() < 0 || worldCell.y() >= heightTiles) { return false; } if (worldCell.x() < leftEdgeX) { return false; } } return true; } bool isPlacementValid(const FactoryState& state, const GameConfig& config,BuildingType type, QPoint anchor, Rotation rotation) { const BuildingDef* def = config.buildings.findBuildingDef(type); if (def == nullptr) { return false; } const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation); if (!bodyCellsWithinWorldBounds(state, config, mask.bodyCells, anchor)) { return false; } // Terrain: ship-dock (S) cells must sit in space (x >= 0); all other body // (A) cells must sit on the asteroid (x < 0). (REQ-BLD-PLACE-VALID) for (const QPoint& cell : mask.bodyCells) { const QPoint worldCell = anchor + cell; bool isShipDock = false; for (const QPoint& dock : mask.shipDockCells) { if (dock == cell) { isShipDock = true; break; } } if (isShipDock) { if (worldCell.x() < 0) { return false; } } else if (worldCell.x() >= 0) { return false; } } return true; } std::optional findRotateInPlaceTarget(const FactoryState& state, const GameConfig& config, BuildingType type, QPoint anchor, Rotation rot) { // Tunnel Entries and Tunnel Exits cannot be rotated in place; re-orienting a // tunnel requires deconstructing and re-placing it (REQ-BLD-ROTATE-IN-PLACE). if (type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit) { return std::nullopt; } const BuildingDef* def = config.buildings.findBuildingDef(type); if (!def) { return std::nullopt; } const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rot); if (mask.bodyCells.empty()) { return std::nullopt; } // All body cells must be occupied by the same entity. const QPoint firstAbs = anchor + mask.bodyCells[0]; const std::optional firstOwner = state.grid.findOwner(firstAbs); if (!firstOwner.has_value()) { return std::nullopt; } const BuildingId candidateId = *firstOwner; for (const QPoint& rel : mask.bodyCells) { const std::optional owner = state.grid.findOwner(anchor + rel); if (!owner.has_value() || *owner != candidateId) { return std::nullopt; } } // Verify the candidate is the same building type with the same cell count. for (const ConstructionSite& site : state.constructionQueue) { if (site.id != candidateId) { continue; } if (site.type != type) { return std::nullopt; } if (site.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; } return candidateId; } for (const Building& b : state.buildings) { if (b.id != candidateId) { continue; } if (b.type != type) { return std::nullopt; } if (b.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; } return candidateId; } return std::nullopt; } bool canPlaceBuilding(const FactoryState& state, const GameConfig& config, BuildingType type, QPoint anchor, Rotation rotation) { // Terrain and world-bounds validity first (REQ-BLD-PLACE-VALID); occupancy is // the extra rule this adds. if (!isPlacementValid(state, config, type, anchor, rotation)) { return false; } const BuildingDef* def = config.buildings.findBuildingDef(type); if (!def) { return false; } const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rotation); bool anyOccupied = false; for (const QPoint& relativeCell : parsed.bodyCells) { if (isTileOccupied(state, anchor + relativeCell)) { anyOccupied = true; break; } } if (anyOccupied) { // Occupied is still placeable when what is there is the same building being // re-oriented (REQ-BLD-ROTATE-IN-PLACE). return findRotateInPlaceTarget(state, config, type, anchor, rotation).has_value(); } return true; } std::vector resolveBeltDragPath(const std::vector& path, const FactoryState& state, const GameConfig& config, int buildingBlocksStock) { std::vector resolved; resolved.reserve(path.size()); const BuildingDef* def = config.buildings.findBuildingDef(BuildingType::Belt); const int beltCost = (def != nullptr) ? def->cost : 0; int spent = 0; for (const BeltPathTile& entry : path) { BeltDragResolved item; const std::optional rotateTarget = findRotateInPlaceTarget(state, config, BuildingType::Belt, entry.tile, entry.rotation); if (rotateTarget.has_value()) { // A tile holding only a belt (or belt site) is re-oriented, no cost. item.action = BeltTileAction::RotateInPlace; item.affordable = true; item.rotateId = rotateTarget; } else if (canPlaceBuilding(state, config, BuildingType::Belt, entry.tile, entry.rotation)) { // Empty, valid cell: a new belt, subject to cumulative affordability. item.action = BeltTileAction::PlaceNew; item.affordable = (spent + beltCost <= buildingBlocksStock); item.rotateId = std::nullopt; if (item.affordable) { spent += beltCost; } } else { // Occupied by a non-belt building/site, or otherwise invalid terrain. item.action = BeltTileAction::Invalid; item.affordable = false; item.rotateId = std::nullopt; } resolved.push_back(item); } return resolved; }