implement building system

This commit is contained in:
2026-04-19 20:50:42 +02:00
parent c70b5c8f08
commit bf29cc40e3
19 changed files with 1818 additions and 7 deletions

View File

@@ -8,6 +8,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/StationsConfig.h
${CMAKE_CURRENT_SOURCE_DIR}/GameConfig.h
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.h
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
PARENT_SCOPE
)
@@ -15,6 +16,7 @@ SET(SRCS
${SRCS}
${CMAKE_CURRENT_SOURCE_DIR}/Formula.cpp
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp
PARENT_SCOPE
)

View File

@@ -221,6 +221,7 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
cfg.beltSpeedTilesPerSecond = requireDouble(tbl["world"]["belt_speed_tiles_per_second"], file, "world.belt_speed_tiles_per_second");

View File

@@ -0,0 +1,172 @@
#include "SurfaceMask.h"
#include <algorithm>
#include <climits>
namespace
{
// Rotate direction character 90° clockwise.
char rotateDirCharCW(char c)
{
switch (c)
{
case '>': return 'v';
case 'v': return '<';
case '<': return '^';
case '^': return '>';
default: return c;
}
}
// Rotate the character grid 90° clockwise.
// Input grid[row][col]. Returns a new grid with swapped dimensions.
std::vector<std::string> rotateCW(const std::vector<std::string>& grid)
{
if (grid.empty())
{
return {};
}
const int srcH = static_cast<int>(grid.size());
// Pad all rows to the same width.
int srcW = 0;
for (const std::string& row : grid)
{
const int w = static_cast<int>(row.size());
if (w > srcW)
{
srcW = w;
}
}
// After 90° CW: new width = srcH, new height = srcW.
const int dstW = srcH;
const int dstH = srcW;
std::vector<std::string> dst(dstH, std::string(dstW, ' '));
for (int row = 0; row < srcH; ++row)
{
for (int col = 0; col < srcW; ++col)
{
const char ch = (col < static_cast<int>(grid[row].size()))
? grid[row][col]
: ' ';
// 90° CW mapping: (col, row) -> (dstCol = srcH-1-row, dstRow = col)
const int dstCol = srcH - 1 - row;
const int dstRow = col;
dst[dstRow][dstCol] = rotateDirCharCW(ch);
}
}
return dst;
}
} // namespace
ParsedSurfaceMask parseSurfaceMask(const std::vector<std::string>& rows,
Rotation rotation)
{
// Number of 90° CW steps to apply.
int cwSteps = 0;
switch (rotation)
{
case Rotation::East: cwSteps = 0; break;
case Rotation::South: cwSteps = 1; break;
case Rotation::West: cwSteps = 2; break;
case Rotation::North: cwSteps = 3; break;
}
// Apply rotations.
std::vector<std::string> grid = rows;
for (int i = 0; i < cwSteps; ++i)
{
grid = rotateCW(grid);
}
// Scan grid: collect body cells, ship dock cells, and output port indicators.
std::vector<QPoint> rawBodyCells;
std::vector<QPoint> rawShipDockCells;
struct RawPort
{
QPoint tile;
Rotation direction;
};
std::vector<RawPort> rawPorts;
for (int row = 0; row < static_cast<int>(grid.size()); ++row)
{
for (int col = 0; col < static_cast<int>(grid[row].size()); ++col)
{
const char ch = grid[row][col];
if (ch == 'A')
{
rawBodyCells.push_back(QPoint(col, row));
}
else if (ch == 'S')
{
rawBodyCells.push_back(QPoint(col, row));
rawShipDockCells.push_back(QPoint(col, row));
}
else if (ch == '>')
{
rawPorts.push_back({QPoint(col, row), Rotation::East});
}
else if (ch == '<')
{
rawPorts.push_back({QPoint(col, row), Rotation::West});
}
else if (ch == '^')
{
rawPorts.push_back({QPoint(col, row), Rotation::North});
}
else if (ch == 'v')
{
rawPorts.push_back({QPoint(col, row), Rotation::South});
}
}
}
// Compute bounding box of body cells and normalize to (0,0).
int minCol = INT_MAX;
int minRow = INT_MAX;
int maxCol = INT_MIN;
int maxRow = INT_MIN;
for (const QPoint& pt : rawBodyCells)
{
if (pt.x() < minCol) { minCol = pt.x(); }
if (pt.x() > maxCol) { maxCol = pt.x(); }
if (pt.y() < minRow) { minRow = pt.y(); }
if (pt.y() > maxRow) { maxRow = pt.y(); }
}
// If there are no body cells, return an empty mask.
if (rawBodyCells.empty())
{
return ParsedSurfaceMask{};
}
const QPoint offset(-minCol, -minRow);
ParsedSurfaceMask result;
result.footprint = QSize(maxCol - minCol + 1, maxRow - minRow + 1);
for (const QPoint& pt : rawBodyCells)
{
result.bodyCells.push_back(pt + offset);
}
for (const QPoint& pt : rawShipDockCells)
{
result.shipDockCells.push_back(pt + offset);
}
for (const RawPort& rp : rawPorts)
{
Port port;
port.tile = rp.tile + offset;
port.direction = rp.direction;
result.outputPorts.push_back(port);
}
return result;
}

View File

@@ -0,0 +1,28 @@
#pragma once
#include <string>
#include <vector>
#include <QPoint>
#include <QSize>
#include "Port.h"
#include "Rotation.h"
// Parsed representation of a building's surface_mask after applying rotation.
// All coordinates are relative to the building's anchor tile (the point passed
// to BuildingSystem::place), which corresponds to (0,0) in this system.
struct ParsedSurfaceMask
{
QSize footprint; // bounding box of body cells (A + S tiles)
std::vector<QPoint> bodyCells; // relative positions of A and S tiles
std::vector<Port> outputPorts; // port.tile = cell adjacent to body, outside footprint;
// port.direction = flow direction away from building
std::vector<QPoint> shipDockCells; // relative positions of S tiles (subset of bodyCells)
};
// Parse a surface_mask definition (as loaded from TOML) and apply the given
// rotation. The canonical mask orientation corresponds to Rotation::East.
// Rotations are applied clockwise (East=0, South=90°, West=180°, North=270°).
ParsedSurfaceMask parseSurfaceMask(const std::vector<std::string>& rows,
Rotation rotation);

View File

@@ -39,6 +39,7 @@ struct WorldConfig
{
int heightTiles; // REQ-GW-HEIGHT
int refundPercentage; // REQ-BLD-DEMOLISH
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
double beltSpeedTilesPerSecond; // REQ-GW-BELT-SPEED

View File

@@ -135,6 +135,31 @@ std::optional<Item> BeltSystem::tryTakeItem(Port port)
return std::nullopt;
}
std::optional<ItemType> BeltSystem::peekItem(Port port) const
{
const std::map<std::pair<int, int>, BeltTile>::const_iterator it =
m_belts.find(key(port.tile));
if (it == m_belts.end())
{
return std::nullopt;
}
if (it->second.direction != port.direction)
{
return std::nullopt;
}
const BeltTile& bt = it->second;
if (bt.front)
{
return bt.front->item.type;
}
if (bt.back)
{
return bt.back->item.type;
}
return std::nullopt;
}
// ---------------------------------------------------------------------------
// Maintenance
// ---------------------------------------------------------------------------

View File

@@ -64,6 +64,10 @@ public:
// Returns nullopt if tile is not a belt, direction mismatches, or tile empty.
std::optional<Item> tryTakeItem(Port port);
// peekItem: return the type of the leading item without removing it.
// Returns nullopt if tile is not a belt, direction mismatches, or tile empty.
std::optional<ItemType> peekItem(Port port) const;
// -- Maintenance ---------------------------------------------------------
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
void tick();

76
src/lib/sim/Building.h Normal file
View File

@@ -0,0 +1,76 @@
#pragma once
#include <map>
#include <optional>
#include <string>
#include <vector>
#include <QPoint>
#include <QSize>
#include "BuildingType.h"
#include "EntityId.h"
#include "Item.h"
#include "ItemType.h"
#include "Port.h"
#include "Rotation.h"
#include "Tick.h"
// Per-material input buffer for a production building.
struct InputBuffer
{
std::map<ItemType, int> counts; // current item counts per material
std::map<ItemType, int> caps; // max items per material (2× per-cycle requirement)
};
// Output buffer shared by all output materials for a production building.
struct OutputBuffer
{
std::vector<Item> items;
int capacity = 0; // 2× per-cycle output; 1× for ReprocessingPlant
};
// Active production cycle for a building.
struct Production
{
std::string recipeId;
Tick completesAt = 0;
std::vector<Item> chosenOutputs; // resolved at cycle start (reprocessing rolls here)
};
// A building placed on the map that is still under construction.
// Occupies tiles but does not produce.
struct ConstructionSite
{
EntityId id = kInvalidEntityId;
QPoint anchor; // top-left of body bounding box
QSize footprint;
std::vector<QPoint> bodyCells; // absolute world tile coordinates
Rotation rotation = Rotation::East;
BuildingType type = BuildingType::Miner;
std::string recipeId; // may be configured before completion
Tick completesAt = 0; // 0 = queued but not yet started
};
// A fully constructed, operational building.
struct Building
{
EntityId id = kInvalidEntityId;
QPoint anchor; // top-left of body bounding box
QSize footprint;
Rotation rotation = Rotation::East;
BuildingType type = BuildingType::Miner;
float hp = 0.0f;
float maxHp = 0.0f;
std::string recipeId; // empty = none selected
InputBuffer inputBuffer;
OutputBuffer outputBuffer;
std::optional<Production> production;
// Pre-computed from surface mask at placement; in absolute world coordinates.
std::vector<QPoint> bodyCells;
std::vector<Port> outputPorts;
std::vector<Port> inputPorts; // perimeter tiles (minus output-port tiles),
// direction pointing INTO building
};

View File

@@ -0,0 +1,661 @@
#include "BuildingSystem.h"
#include <cassert>
#include <random>
#include <set>
#include "SurfaceMask.h"
BuildingSystem::BuildingSystem(const GameConfig& config,
BeltSystem& belts,
std::function<EntityId()> allocateId,
std::function<void(int)> addBuildingBlocks,
std::mt19937& rng)
: m_config(config)
, m_belts(belts)
, m_allocateId(std::move(allocateId))
, m_addBuildingBlocks(std::move(addBuildingBlocks))
, m_rng(rng)
{
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
const BuildingDef* BuildingSystem::findBuildingDef(BuildingType type) const
{
for (const BuildingDef& def : m_config.buildings.buildings)
{
if (def.type == type)
{
return &def;
}
}
return nullptr;
}
const RecipeDef* BuildingSystem::findRecipe(const std::string& id,
BuildingType type) const
{
for (const RecipeDef& recipe : m_config.recipes.recipes)
{
if (recipe.id == id && recipe.building == type)
{
return &recipe;
}
}
return nullptr;
}
void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
{
b.inputBuffer.counts.clear();
b.inputBuffer.caps.clear();
for (const RecipeIngredient& ing : recipe.inputs)
{
const ItemType type{ing.item};
b.inputBuffer.counts[type] = 0;
b.inputBuffer.caps[type] = 2 * ing.amount;
}
b.outputBuffer.items.clear();
if (b.type == BuildingType::ReprocessingPlant)
{
// 1× max-per-roll (REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
int maxAmount = 0;
for (const RecipeOutput& out : recipe.outputs)
{
if (out.amount > maxAmount)
{
maxAmount = out.amount;
}
}
b.outputBuffer.capacity = maxAmount;
}
else
{
// 2× per-cycle output.
int totalAmount = 0;
for (const RecipeOutput& out : recipe.outputs)
{
totalAmount += out.amount;
}
b.outputBuffer.capacity = 2 * totalAmount;
}
}
std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
{
// Build lookup sets for quick membership checks.
std::set<std::pair<int, int>> bodySet;
for (const QPoint& cell : b.bodyCells)
{
bodySet.insert({cell.x(), cell.y()});
}
std::set<std::pair<int, int>> outputPortTiles;
for (const Port& port : b.outputPorts)
{
outputPortTiles.insert({port.tile.x(), port.tile.y()});
}
// Neighbour deltas and the corresponding "inward" belt direction.
const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = { 0, 0, -1, 1};
const Rotation inward[4] = {
Rotation::East, // neighbour is to the West; belt flows East toward building
Rotation::West, // neighbour is to the East; belt flows West toward building
Rotation::South, // neighbour is above (row-1); belt flows South toward building
Rotation::North // neighbour is below (row+1); belt flows North toward building
};
std::set<std::pair<int, int>> seen;
std::vector<Port> inputPorts;
for (const QPoint& cell : b.bodyCells)
{
for (int i = 0; i < 4; ++i)
{
const int nx = cell.x() + dx[i];
const int ny = cell.y() + dy[i];
const std::pair<int, int> neighbor = {nx, ny};
if (bodySet.count(neighbor)) { continue; }
if (outputPortTiles.count(neighbor)){ continue; }
if (seen.count(neighbor)) { continue; }
seen.insert(neighbor);
Port port;
port.tile = QPoint(nx, ny);
port.direction = inward[i];
inputPorts.push_back(port);
}
}
return inputPorts;
}
std::vector<Item> BuildingSystem::rollReprocessingOutput(const RecipeDef& recipe)
{
std::vector<double> weights;
weights.reserve(recipe.outputs.size());
for (const RecipeOutput& out : recipe.outputs)
{
weights.push_back(out.probability.value_or(1.0));
}
std::discrete_distribution<int> dist(weights.begin(), weights.end());
const int idx = dist(m_rng);
const RecipeOutput& chosen = recipe.outputs[static_cast<std::size_t>(idx)];
std::vector<Item> result;
Item item;
item.type.id = chosen.item;
for (int i = 0; i < chosen.amount; ++i)
{
result.push_back(item);
}
return result;
}
// ---------------------------------------------------------------------------
// Placement
// ---------------------------------------------------------------------------
EntityId BuildingSystem::place(BuildingType type, QPoint anchor,
Rotation rotation, Tick currentTick)
{
const EntityId id = m_allocateId();
if (type == BuildingType::Belt)
{
m_belts.placeBelt(anchor, rotation);
m_tileOccupancy[{anchor.x(), anchor.y()}] = id;
m_beltEntities[id] = BeltEntry{anchor, BuildingType::Belt};
return id;
}
if (type == BuildingType::Splitter)
{
const BuildingDef* def = findBuildingDef(type);
assert(def != nullptr);
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
assert(mask.outputPorts.size() >= 2);
const Rotation outA = mask.outputPorts[0].direction;
const Rotation outB = mask.outputPorts[1].direction;
m_belts.placeSplitter(anchor, outA, outB);
m_tileOccupancy[{anchor.x(), anchor.y()}] = id;
m_beltEntities[id] = BeltEntry{anchor, BuildingType::Splitter};
return id;
}
const BuildingDef* def = findBuildingDef(type);
assert(def != nullptr);
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
// Record tile occupancy for body cells.
for (const QPoint& cell : mask.bodyCells)
{
const QPoint absCell = anchor + cell;
m_tileOccupancy[{absCell.x(), absCell.y()}] = id;
}
// Build construction site.
ConstructionSite site;
site.id = id;
site.anchor = anchor;
site.footprint = mask.footprint;
site.rotation = rotation;
site.type = type;
for (const QPoint& cell : mask.bodyCells)
{
site.bodyCells.push_back(anchor + cell);
}
if (m_constructionQueue.empty())
{
site.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
}
// else: completesAt remains 0 (queued, not yet started).
m_constructionQueue.push_back(std::move(site));
return id;
}
// ---------------------------------------------------------------------------
// Demolish
// ---------------------------------------------------------------------------
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?
for (std::deque<ConstructionSite>::iterator it = m_constructionQueue.begin();
it != m_constructionQueue.end();
++it)
{
if (it->id == id)
{
const BuildingDef* def = findBuildingDef(it->type);
for (const QPoint& cell : it->bodyCells)
{
m_tileOccupancy.erase({cell.x(), cell.y()});
}
m_constructionQueue.erase(it);
if (def)
{
return def->cost * m_config.world.refundPercentage / 100;
}
return 0;
}
}
// Operational building?
for (std::vector<Building>::iterator it = m_buildings.begin();
it != m_buildings.end();
++it)
{
if (it->id == id)
{
const BuildingDef* def = findBuildingDef(it->type);
for (const QPoint& cell : it->bodyCells)
{
m_tileOccupancy.erase({cell.x(), cell.y()});
}
m_buildings.erase(it);
if (def)
{
return def->cost * m_config.world.refundPercentage / 100;
}
return 0;
}
}
return 0;
}
// ---------------------------------------------------------------------------
// Set recipe
// ---------------------------------------------------------------------------
void BuildingSystem::setRecipe(EntityId id, const std::string& recipeId)
{
// Construction site: store recipe for when building completes.
for (ConstructionSite& site : m_constructionQueue)
{
if (site.id == id)
{
site.recipeId = recipeId;
return;
}
}
// Operational building: clear buffers and re-init.
for (Building& building : m_buildings)
{
if (building.id == id)
{
building.recipeId = recipeId;
building.inputBuffer.counts.clear();
building.inputBuffer.caps.clear();
building.outputBuffer.items.clear();
building.outputBuffer.capacity = 0;
building.production = std::nullopt;
if (!recipeId.empty())
{
const RecipeDef* recipe = findRecipe(recipeId, building.type);
if (recipe)
{
initBuffers(building, *recipe);
}
}
return;
}
}
}
// ---------------------------------------------------------------------------
// Tick hooks
// ---------------------------------------------------------------------------
void BuildingSystem::tickConstruction(Tick currentTick)
{
if (m_constructionQueue.empty())
{
return;
}
ConstructionSite& front = m_constructionQueue.front();
// Guard: if somehow the front site was never started, start it now.
if (front.completesAt == 0)
{
const BuildingDef* def = findBuildingDef(front.type);
if (def)
{
front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
}
return;
}
if (currentTick < front.completesAt)
{
return;
}
// Promote construction site to 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())
{
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
if (recipe)
{
initBuffers(building, *recipe);
}
}
m_buildings.push_back(std::move(building));
m_constructionQueue.pop_front();
// Start next queued site if present.
if (!m_constructionQueue.empty() && m_constructionQueue.front().completesAt == 0)
{
const BuildingDef* nextDef = findBuildingDef(m_constructionQueue.front().type);
if (nextDef)
{
m_constructionQueue.front().completesAt =
currentTick + secondsToTicks(nextDef->constructionTimeSeconds);
}
}
}
void BuildingSystem::tickBeltPull()
{
for (Building& building : m_buildings)
{
// HQ: pull building_block items and add to global stock.
if (building.type == BuildingType::Hq)
{
for (const Port& port : building.inputPorts)
{
const std::optional<ItemType> peeked = m_belts.peekItem(port);
if (peeked && peeked->id == "building_block")
{
const std::optional<Item> taken = m_belts.tryTakeItem(port);
if (taken)
{
m_addBuildingBlocks(1);
}
}
}
continue;
}
if (building.recipeId.empty())
{
continue;
}
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
if (!recipe || recipe->inputs.empty())
{
continue;
}
for (const Port& port : building.inputPorts)
{
const std::optional<ItemType> peeked = m_belts.peekItem(port);
if (!peeked)
{
continue;
}
const ItemType& type = *peeked;
// Accept only if this type is a required input and buffer has space.
const std::map<ItemType, int>::const_iterator capIt =
building.inputBuffer.caps.find(type);
if (capIt == building.inputBuffer.caps.end() || capIt->second == 0)
{
continue;
}
const int current = [&]() -> int
{
const std::map<ItemType, int>::const_iterator it =
building.inputBuffer.counts.find(type);
return (it != building.inputBuffer.counts.end()) ? it->second : 0;
}();
if (current >= capIt->second)
{
continue;
}
const std::optional<Item> taken = m_belts.tryTakeItem(port);
if (taken)
{
building.inputBuffer.counts[taken->type]++;
}
}
}
}
void BuildingSystem::tickProduction(Tick currentTick)
{
for (Building& building : m_buildings)
{
// Skip types without a recipe-based production loop.
if (building.type == BuildingType::Belt ||
building.type == BuildingType::Splitter ||
building.type == BuildingType::Shipyard ||
building.type == BuildingType::SalvageBay ||
building.type == BuildingType::Hq ||
building.type == BuildingType::PlayerDefenceStation ||
building.type == BuildingType::EnemyDefenceStation)
{
continue;
}
if (building.recipeId.empty())
{
continue;
}
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
if (!recipe)
{
continue;
}
// If a production cycle is active, check for completion.
if (building.production)
{
if (currentTick >= building.production->completesAt)
{
for (const Item& item : building.production->chosenOutputs)
{
building.outputBuffer.items.push_back(item);
}
building.production = std::nullopt;
}
// Whether we just completed or are still running, do not start
// another cycle in the same tick.
continue;
}
// Idle: check if a new cycle can start.
// 1. All required inputs present?
bool inputsOk = true;
for (const RecipeIngredient& ing : recipe->inputs)
{
const ItemType type{ing.item};
const std::map<ItemType, int>::const_iterator it =
building.inputBuffer.counts.find(type);
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
if (have < ing.amount)
{
inputsOk = false;
break;
}
}
if (!inputsOk)
{
continue;
}
// 2. Determine chosen outputs (roll for reprocessing).
std::vector<Item> chosen;
if (building.type == BuildingType::ReprocessingPlant)
{
chosen = rollReprocessingOutput(*recipe);
}
else
{
for (const RecipeOutput& out : recipe->outputs)
{
Item item;
item.type.id = out.item;
for (int i = 0; i < out.amount; ++i)
{
chosen.push_back(item);
}
}
}
// 3. Output buffer has space for chosen outputs?
const int newSize = static_cast<int>(building.outputBuffer.items.size())
+ static_cast<int>(chosen.size());
if (newSize > building.outputBuffer.capacity)
{
continue;
}
// 4. Consume inputs and start cycle.
for (const RecipeIngredient& ing : recipe->inputs)
{
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
}
Production prod;
prod.recipeId = building.recipeId;
prod.completesAt = currentTick + secondsToTicks(recipe->durationSeconds);
prod.chosenOutputs = std::move(chosen);
building.production = std::move(prod);
}
}
void BuildingSystem::tickBeltPush()
{
for (Building& building : m_buildings)
{
if (building.outputBuffer.items.empty())
{
continue;
}
for (const Port& outputPort : building.outputPorts)
{
if (building.outputBuffer.items.empty())
{
break;
}
const Item item = building.outputBuffer.items.front();
if (m_belts.tryPutItem(outputPort, item))
{
building.outputBuffer.items.erase(building.outputBuffer.items.begin());
}
}
}
}
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
const Building* BuildingSystem::findBuilding(EntityId id) const
{
for (const Building& building : m_buildings)
{
if (building.id == id)
{
return &building;
}
}
return nullptr;
}
const ConstructionSite* BuildingSystem::findSite(EntityId id) const
{
for (const ConstructionSite& site : m_constructionQueue)
{
if (site.id == id)
{
return &site;
}
}
return nullptr;
}
std::vector<Building> BuildingSystem::allBuildings() const
{
return m_buildings;
}
std::vector<ConstructionSite> BuildingSystem::allSites() const
{
return std::vector<ConstructionSite>(m_constructionQueue.begin(),
m_constructionQueue.end());
}
bool BuildingSystem::isTileOccupied(QPoint tile) const
{
return m_tileOccupancy.count({tile.x(), tile.y()}) > 0;
}

View File

@@ -0,0 +1,88 @@
#pragma once
#include <deque>
#include <functional>
#include <map>
#include <optional>
#include <random>
#include <string>
#include <utility>
#include <vector>
#include <QPoint>
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingType.h"
#include "EntityId.h"
#include "GameConfig.h"
#include "Rotation.h"
#include "Tick.h"
// Manages building placement, construction queuing, and the per-tick
// production loop (belt→building pull, production, building→belt push).
// Belt and Splitter types are forwarded to BeltSystem rather than stored
// as Building instances.
class BuildingSystem
{
public:
BuildingSystem(const GameConfig& config,
BeltSystem& belts,
std::function<EntityId()> allocateId,
std::function<void(int)> addBuildingBlocks,
std::mt19937& rng);
// -- Placement / demolish ------------------------------------------------
// Returns the new entity id. Belt and Splitter register with BeltSystem
// directly; other types enter the construction queue.
EntityId place(BuildingType type, QPoint anchor, Rotation rotation,
Tick currentTick);
// 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(EntityId id);
// Set the recipe (or blueprint id for shipyard) on a building or queued
// construction site. Clears both buffers on an operational building.
void setRecipe(EntityId id, const std::string& recipeId);
// -- Tick hooks (called from Simulation::tick in the documented order) ---
void tickConstruction(Tick currentTick);
void tickBeltPull();
void tickProduction(Tick currentTick);
void tickBeltPush();
// -- Queries -------------------------------------------------------------
const Building* findBuilding(EntityId id) const;
const ConstructionSite* findSite(EntityId id) const;
std::vector<Building> allBuildings() const;
std::vector<ConstructionSite> allSites() const;
bool isTileOccupied(QPoint tile) const;
private:
struct BeltEntry
{
QPoint tile;
BuildingType type; // Belt or Splitter
};
const BuildingDef* findBuildingDef(BuildingType type) const;
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
void initBuffers(Building& b, const RecipeDef& recipe) const;
std::vector<Port> computeInputPorts(const Building& b) const;
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
const GameConfig& m_config;
BeltSystem& m_belts;
std::function<EntityId()> m_allocateId;
std::function<void(int)> m_addBuildingBlocks;
std::mt19937& m_rng;
std::vector<Building> m_buildings;
std::deque<ConstructionSite> m_constructionQueue;
std::map<EntityId, BeltEntry> m_beltEntities;
// Maps every occupied body-cell coordinate to the entity that owns it.
std::map<std::pair<int, int>, EntityId> m_tileOccupancy;
};

View File

@@ -3,6 +3,8 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.h
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.h
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
PARENT_SCOPE
)
@@ -11,6 +13,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/Simulation.cpp
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
PARENT_SCOPE
)

View File

@@ -1,15 +1,33 @@
#include "Simulation.h"
#include "BuildingSystem.h"
Simulation::Simulation(const GameConfig& config, unsigned int seed)
: m_config(config)
, m_rng(seed)
, m_currentTick(0)
, m_nextId(1)
, m_buildingBlocksStock(config.world.startingBuildingBlocks)
, m_beltSystem(config.world.beltSpeedTilesPerSecond)
{
m_buildingSystem = std::make_unique<BuildingSystem>(
config,
m_beltSystem,
[this]() { return allocateId(); },
[this](int amount) { m_buildingBlocksStock += amount; },
m_rng);
}
Simulation::~Simulation() = default;
void Simulation::tick()
{
m_buildingSystem->tickConstruction(m_currentTick);
m_buildingSystem->tickBeltPull(); // tick order step 3
m_buildingSystem->tickProduction(m_currentTick); // step 4
m_buildingSystem->tickBeltPush(); // step 5
m_beltSystem.tick(); // step 6
++m_currentTick;
}
@@ -32,6 +50,31 @@ Tick Simulation::currentTick() const
return m_currentTick;
}
int Simulation::buildingBlocksStock() const
{
return m_buildingBlocksStock;
}
BuildingSystem& Simulation::buildings()
{
return *m_buildingSystem;
}
const BuildingSystem& Simulation::buildings() const
{
return *m_buildingSystem;
}
BeltSystem& Simulation::belts()
{
return m_beltSystem;
}
const BeltSystem& Simulation::belts() const
{
return m_beltSystem;
}
EntityId Simulation::allocateId()
{
return m_nextId++;

View File

@@ -1,21 +1,25 @@
#pragma once
#include <memory>
#include <random>
#include <vector>
#include "BeltSystem.h"
#include "BlueprintDropEvent.h"
#include "EntityId.h"
#include "FireEvent.h"
#include "BlueprintDropEvent.h"
#include "GameConfig.h"
#include "Tick.h"
class BuildingSystem;
class Simulation
{
public:
explicit Simulation(const GameConfig& config, unsigned int seed = 0);
~Simulation();
// Advances the simulation by one tick. Tick order per architecture.md §Tick Order.
// Currently a stub; subsystems are plugged in across Steps 3-7.
void tick();
// Returns all fire events accumulated since the last drain, clearing the
@@ -26,6 +30,12 @@ public:
std::vector<BlueprintDropEvent> drainBlueprintDropEvents();
Tick currentTick() const;
int buildingBlocksStock() const;
BuildingSystem& buildings();
const BuildingSystem& buildings() const;
BeltSystem& belts();
const BeltSystem& belts() const;
private:
EntityId allocateId(); // Strictly increasing; never returns kInvalidEntityId.
@@ -33,9 +43,13 @@ private:
const GameConfig& m_config;
std::mt19937 m_rng;
Tick m_currentTick;
EntityId m_nextId; // starts at 1; 0 is kInvalidEntityId.
Tick m_currentTick;
EntityId m_nextId;
int m_buildingBlocksStock;
std::vector<FireEvent> m_fireEvents;
BeltSystem m_beltSystem;
std::unique_ptr<BuildingSystem> m_buildingSystem;
std::vector<FireEvent> m_fireEvents;
std::vector<BlueprintDropEvent> m_blueprintDropEvents;
};