#pragma once #include #include #include #include #include #include #include #include #include #include #include #include "BeltSystem.h" #include "Building.h" #include "BuildingType.h" #include "BuildingId.h" #include "GameConfig.h" #include "Rotation.h" #include "ModulesConfig.h" #include "ShipLayout.h" #include "ShipsConfig.h" #include "Tick.h" class Hasher; // Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT). // The simulation owns the classification so it stays in sync with the // production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill // color. enum class ProductionStatus { Unconfigured, // no recipe/schematic selected (grey) Producing, // a production cycle is active (green) Starved, // idle: a required input is missing / Salvage Bay empty (red) Blocked, // idle: output buffer full, inputs otherwise present (yellow) }; // Manages building placement, construction queuing, and the per-tick // production loop (belt→building pull, production, building→belt push). // All types including Belt and Splitter are stored as Building instances; // BeltSystem owns the per-tile simulation data (item slots, flow). class BuildingSystem { public: BuildingSystem(const GameConfig& config, BeltSystem& belts, std::function allocateBuildingId, std::function addBuildingBlocks, std::function&)> spawnShip, std::function isItemUnlocked, std::mt19937& rng); // -- Placement / demolish ------------------------------------------------ // Returns the new entity id, or nullopt if the placement falls outside the // world bounds (vertical extent and asteroid left edge). Belt and Splitter // register with BeltSystem directly; other types enter the construction // queue. Terrain type (A vs S) is NOT checked here so that tests can stage // arbitrary layouts; the player-facing entry point // (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid. std::optional place(BuildingType type, QPoint anchor, Rotation rotation, Tick currentTick); // Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and // world-bounds rules: every ship-dock (S) cell sits in space (x >= 0), every // other body (A) cell sits on the asteroid (x < 0 and x >= the left edge), // and every cell has 0 <= y < world.height_tiles. There is no right-side // bound — space extends rightward. Tile occupancy is NOT checked here. bool isPlacementValid(BuildingType type, QPoint anchor, Rotation rotation) const; // Sets the current buildable asteroid width in tiles. Grows the left // placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK). // Defaults to world.regions.asteroid_width_tiles at construction. void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; } // Remove a building or construction site by id. Returns the refund in // building blocks (floor(cost * refundPercentage / 100)). Returns 0 for // unknown ids. int demolish(BuildingId id); // Set the recipe (or schematic id for shipyard) on a building or queued // construction site. Clears both buffers on an operational building. void setRecipe(BuildingId id, const std::string& recipeId); // Set the module layout for a shipyard. Cancels in-progress production // (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD). void setShipLayout(BuildingId id, const ShipLayoutConfig& layout); // Splitter filter configuration for a queued/under-construction Splitter // site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through // BeltSystem by tile; these mirror that for sites, which are not yet // registered with BeltSystem. getSiteSplitterInfo returns the site's two // output directions (derived from its surface mask) and stored filters, or // nullopt if the id is not a Splitter site. The stored filters are applied // to BeltSystem when the splitter finishes building (tickConstruction). std::optional getSiteSplitterInfo(BuildingId id) const; void setSiteSplitterFilters(BuildingId id, const std::vector& filterA, const std::vector& filterB); // -- Tick hooks (called from Simulation::tick in the documented order) --- void tickConstruction(Tick currentTick); void tickBeltPull(); void tickProduction(Tick currentTick); void tickShipyardProduction(Tick currentTick); // Advances each building's virtual output belts, hands finished items off onto // the adjacent real belt, and feeds new buffered items into them // (REQ-MAT-OUTPUT-EMERGE). void tickOutputBelts(); // -- Queries ------------------------------------------------------------- struct BeltTileInfo { BuildingId buildingId; 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 Building* findBuilding(BuildingId id) const; const ConstructionSite* findSite(BuildingId id) const; std::vector getAllBuildings() const; std::vector getAllSites() const; // REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed // (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings. int getProductionBuildingCount() const; // REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above // that currently has an active production cycle. int getActiveProductionBuildingCount() const; // Production state for the UI status light (REQ-UI-STATUS-LIGHT). Returns // nullopt for building types that show no light (belts, splitters, tunnels, // HQ, defence stations). The Salvage Bay is a two-state special case: // Producing while its output buffer holds scrap, Starved when empty. std::optional getProductionStatus(const Building& building) const; std::vector getAllBeltTiles() const; bool isTileOccupied(QPoint tile) const; // Visits every item currently emerging from a building output port on its // virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its // world-space centre (in tile units). Least-progressed first (drawn bottom) so // callers can paint in visit order (REQ-GW-TILE-SIZE ordering). void forEachEmergingItem( const std::function& visit) const; // Visits every item currently travelling inward on a building input port's // virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its // world-space centre (in tile units). Least-progressed first (drawn bottom). void forEachIncomingItem( const std::function& visit) const; // Returns the entity id of the building or construction site whose footprint // exactly coincides with the ghost (type, anchor, rot) and is of the same // building type. Returns nullopt otherwise. std::optional findRotateInPlaceTarget(BuildingType type, QPoint anchor, Rotation rot) const; // Rotate an existing building or construction site to newRotation in place. // For belt-type operational buildings, re-registers with BeltSystem (items // currently on the tile are discarded by BeltSystem::removeTile). void rotateInPlace(BuildingId id, Rotation newRotation); // 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 getInputPorts(BuildingId id) const; // Register / unregister tile occupancy for ECS station entities. void registerTileOccupancy(const std::vector& cells, BuildingId ownerPlaceholder); void unregisterTileOccupancy(const std::vector& cells); // Place one "scrap" item into a SalvageBay's output buffer. // Returns false if bay not found, wrong type, or output buffer is full. bool deliverScrapToSalvageBay(BuildingId bayId); // Bypass the construction queue and create a fully-operational Building // immediately. Used for pre-placed structures (HQ, defence stations). // surfaceMask comes from the relevant config struct. BuildingId placeImmediate(BuildingType type, const std::vector& surfaceMask, QPoint anchor, Rotation rotation); // Remove an operational building by id without refund (used for deaths). // Returns true if found and removed. bool removeBuilding(BuildingId id); // Mutable iteration over all operational buildings. void forEachBuilding(std::function fn); // -- Determinism --------------------------------------------------------- // Folds all building, construction-site, and tile-occupancy state into the // hasher in deterministic order (see docs/replay_design.md). void appendChecksum(Hasher& hasher) const; private: Building* findBuildingMutable(BuildingId id); // True if the consumer would accept `type` at the given input port right now: // it is a required input (or a building block for the HQ), the reservation-aware // buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE). bool canAcceptInput(const Building& consumer, std::size_t inputPortIndex, const ItemType& type) const; // Places an accepted item onto the consumer's input belt at progress 0.0, // reserving a per-material buffer slot (REQ-MAT-INPUT-INTAKE). void depositToInputBelt(Building& consumer, std::size_t inputPortIndex, const Item& item); // Attempts to hand an emerging output item straight into a directly adjacent // building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE). // Returns true if the item was accepted onto the consumer's input belt. bool tryDirectCoupleDeposit(BuildingId producerId, const Port& outputPort, const Item& item); // Candidate recipes an idle building would try this tick: an auto-recipe // building (Smelter, Reprocessing Plant) offers every recipe of its type with // inputs; other buildings offer only their selected recipe. Shared by // tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT). std::vector gatherCandidateRecipes(const Building& b) const; // True if every input of `recipe` is present in `b`'s input buffers in the // required per-cycle amount (REQ-MAT-CYCLE input check). bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe) const; // Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD). std::map computeShipyardRequiredMaterials(const Building& b) const; // True if the building currently has all inputs/materials to start a cycle // (ignoring output-buffer space); drives the Starved/Blocked distinction of // the status light (REQ-UI-STATUS-LIGHT). bool hasInputsToStart(const Building& b) const; const BuildingDef* findBuildingDef(BuildingType type) const; const RecipeDef* findRecipe(const std::string& id, BuildingType type) const; const ShipDef* findShipDef(const std::string& id) const; const ModuleDef* findModuleDef(const std::string& id) const; void initBuffers(Building& b, const RecipeDef& recipe) const; // Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input // caps span the union of every recipe of the building's type; no player // recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). void initAutoBuffers(Building& b) const; void initShipyardBuffers(Building& b) const; void initSalvageBayBuffer(Building& b) const; std::vector computeInputPorts(const Building& b) const; // Core input-edge scan shared by operational buildings and construction sites. std::vector computeInputPorts(const std::vector& bodyCells, const std::vector& outputPorts) const; std::vector rollReprocessingOutput(const RecipeDef& recipe); bool bodyCellsWithinWorldBounds( const std::vector& bodyCells, QPoint anchor) const; const GameConfig& m_config; BeltSystem& m_belts; std::function m_allocateBuildingId; std::function m_addBuildingBlocks; std::function&)> m_spawnShip; std::function m_isItemUnlocked; std::mt19937& m_rng; int m_asteroidWidth_tiles; std::vector m_buildings; std::deque m_constructionQueue; // Maps every occupied body-cell coordinate to the entity that owns it. std::map, BuildingId> m_tileOccupancy; };