949 lines
27 KiB
C++
949 lines
27 KiB
C++
#include "BuildingSystem.h"
|
||
|
||
#include <cassert>
|
||
#include <limits>
|
||
#include <random>
|
||
#include <set>
|
||
|
||
#include "SurfaceMask.h"
|
||
|
||
BuildingSystem::BuildingSystem(const GameConfig& config,
|
||
BeltSystem& belts,
|
||
std::function<EntityId()> allocateId,
|
||
std::function<void(int)> addBuildingBlocks,
|
||
std::function<void(const std::string&, QVector2D)> spawnShip,
|
||
std::mt19937& rng)
|
||
: m_config(config)
|
||
, m_belts(belts)
|
||
, m_allocateId(std::move(allocateId))
|
||
, m_addBuildingBlocks(std::move(addBuildingBlocks))
|
||
, m_spawnShip(std::move(spawnShip))
|
||
, 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;
|
||
}
|
||
|
||
const ShipDef* BuildingSystem::findShipDef(const std::string& id) const
|
||
{
|
||
for (const ShipDef& def : m_config.ships.ships)
|
||
{
|
||
if (def.id == id)
|
||
{
|
||
return &def;
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
|
||
void BuildingSystem::initShipyardBuffers(Building& b) const
|
||
{
|
||
b.inputBuffer.counts.clear();
|
||
b.inputBuffer.caps.clear();
|
||
b.outputBuffer.items.clear();
|
||
b.outputBuffer.capacity = 0;
|
||
const ShipDef* def = findShipDef(b.recipeId);
|
||
if (!def)
|
||
{
|
||
return;
|
||
}
|
||
for (const RecipeIngredient& ing : def->schematic.materials)
|
||
{
|
||
const ItemType type{ing.item};
|
||
b.inputBuffer.counts[type] = 0;
|
||
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||
}
|
||
}
|
||
|
||
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();
|
||
|
||
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)
|
||
{
|
||
// 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;
|
||
}
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// Operational building?
|
||
for (std::vector<Building>::iterator it = m_buildings.begin();
|
||
it != m_buildings.end();
|
||
++it)
|
||
{
|
||
if (it->id == id)
|
||
{
|
||
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
|
||
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
|
||
{
|
||
m_belts.removeTile(it->anchor);
|
||
}
|
||
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())
|
||
{
|
||
if (building.type == BuildingType::Shipyard)
|
||
{
|
||
initShipyardBuffers(building);
|
||
}
|
||
else
|
||
{
|
||
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 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)
|
||
{
|
||
m_belts.placeBelt(front.anchor, front.rotation);
|
||
}
|
||
else if (front.type == BuildingType::Splitter)
|
||
{
|
||
assert(mask.outputPorts.size() >= 2);
|
||
m_belts.placeSplitter(front.anchor,
|
||
mask.outputPorts[0].direction,
|
||
mask.outputPorts[1].direction);
|
||
}
|
||
else if (front.type == BuildingType::TunnelEntry)
|
||
{
|
||
m_belts.placeTunnelEntry(front.anchor, front.rotation, m_config.world.tunnelMaxDistance);
|
||
}
|
||
else if (front.type == BuildingType::TunnelExit)
|
||
{
|
||
m_belts.placeTunnelExit(front.anchor, front.rotation);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
if (building.type != BuildingType::Shipyard)
|
||
{
|
||
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::tickShipyardProduction(Tick currentTick)
|
||
{
|
||
for (Building& building : m_buildings)
|
||
{
|
||
if (building.type != BuildingType::Shipyard)
|
||
{
|
||
continue;
|
||
}
|
||
if (building.recipeId.empty())
|
||
{
|
||
continue;
|
||
}
|
||
const ShipDef* shipDef = findShipDef(building.recipeId);
|
||
if (!shipDef)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// If a cycle is in progress, check for completion.
|
||
if (building.production)
|
||
{
|
||
if (currentTick >= building.production->completesAt)
|
||
{
|
||
if (!building.outputPorts.empty())
|
||
{
|
||
const Port& p = building.outputPorts[0];
|
||
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
||
m_spawnShip(building.recipeId, spawnPos);
|
||
}
|
||
building.production = std::nullopt;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// Idle: check if all materials are available to start a new cycle.
|
||
bool inputsOk = true;
|
||
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
||
{
|
||
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;
|
||
}
|
||
|
||
// Consume materials and start the production cycle.
|
||
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
||
{
|
||
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
|
||
}
|
||
|
||
Production prod;
|
||
prod.recipeId = building.recipeId;
|
||
prod.completesAt = currentTick
|
||
+ secondsToTicks(shipDef->schematic.productionTimeSeconds);
|
||
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.tile, item, outputPort.direction))
|
||
{
|
||
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());
|
||
}
|
||
|
||
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const
|
||
{
|
||
std::vector<BeltTileInfo> result;
|
||
for (const Building& b : m_buildings)
|
||
{
|
||
if (b.type != BuildingType::Belt && b.type != BuildingType::Splitter)
|
||
{
|
||
continue;
|
||
}
|
||
BeltTileInfo info;
|
||
info.id = b.id;
|
||
info.tile = b.bodyCells.empty() ? b.anchor : b.bodyCells[0];
|
||
info.type = b.type;
|
||
if (!b.outputPorts.empty())
|
||
{
|
||
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);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
bool BuildingSystem::isTileOccupied(QPoint tile) const
|
||
{
|
||
return m_tileOccupancy.count({tile.x(), tile.y()}) > 0;
|
||
}
|
||
|
||
const Building* BuildingSystem::findNearestBuilding(QVector2D worldPos,
|
||
BuildingType type) const
|
||
{
|
||
const Building* best = nullptr;
|
||
float bestDist = std::numeric_limits<float>::max();
|
||
for (const Building& b : m_buildings)
|
||
{
|
||
if (b.type != type)
|
||
{
|
||
continue;
|
||
}
|
||
QVector2D center(b.anchor.x() + b.footprint.width() / 2.0f,
|
||
b.anchor.y() + b.footprint.height() / 2.0f);
|
||
float dist = (center - worldPos).length();
|
||
if (dist < bestDist)
|
||
{
|
||
bestDist = dist;
|
||
best = &b;
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
bool BuildingSystem::deliverScrapToSalvageBay(EntityId bayId)
|
||
{
|
||
Building* bay = nullptr;
|
||
for (Building& b : m_buildings)
|
||
{
|
||
if (b.id == bayId)
|
||
{
|
||
bay = &b;
|
||
break;
|
||
}
|
||
}
|
||
if (!bay || bay->type != BuildingType::SalvageBay)
|
||
{
|
||
return false;
|
||
}
|
||
if (static_cast<int>(bay->outputBuffer.items.size()) >= bay->outputBuffer.capacity)
|
||
{
|
||
return false;
|
||
}
|
||
bay->outputBuffer.items.push_back(Item{ItemType{"scrap"}});
|
||
return true;
|
||
}
|
||
|
||
void BuildingSystem::healBuilding(EntityId id, float amount)
|
||
{
|
||
for (Building& b : m_buildings)
|
||
{
|
||
if (b.id == id)
|
||
{
|
||
b.hp = std::min(b.hp + amount, b.maxHp);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void BuildingSystem::damageBuilding(EntityId id, float amount)
|
||
{
|
||
for (Building& b : m_buildings)
|
||
{
|
||
if (b.id == id)
|
||
{
|
||
b.hp -= amount;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
EntityId BuildingSystem::placeImmediate(BuildingType type,
|
||
const std::vector<std::string>& surfaceMask,
|
||
QPoint anchor, Rotation rotation,
|
||
float hp, float maxHp)
|
||
{
|
||
const EntityId id = m_allocateId();
|
||
const ParsedSurfaceMask mask = parseSurfaceMask(surfaceMask, rotation);
|
||
|
||
Building building;
|
||
building.id = id;
|
||
building.anchor = anchor;
|
||
building.footprint = mask.footprint;
|
||
building.rotation = rotation;
|
||
building.type = type;
|
||
building.hp = hp;
|
||
building.maxHp = maxHp;
|
||
|
||
for (const QPoint& cell : mask.bodyCells)
|
||
{
|
||
const QPoint absCell = anchor + cell;
|
||
building.bodyCells.push_back(absCell);
|
||
m_tileOccupancy[{absCell.x(), absCell.y()}] = id;
|
||
}
|
||
for (const Port& port : mask.outputPorts)
|
||
{
|
||
Port absPort;
|
||
absPort.tile = anchor + port.tile;
|
||
absPort.direction = port.direction;
|
||
building.outputPorts.push_back(absPort);
|
||
}
|
||
building.inputPorts = computeInputPorts(building);
|
||
|
||
m_buildings.push_back(std::move(building));
|
||
return id;
|
||
}
|
||
|
||
bool BuildingSystem::removeBuilding(EntityId id)
|
||
{
|
||
for (std::vector<Building>::iterator it = m_buildings.begin();
|
||
it != m_buildings.end();
|
||
++it)
|
||
{
|
||
if (it->id == id)
|
||
{
|
||
if (it->type == BuildingType::Belt || it->type == BuildingType::Splitter
|
||
|| it->type == BuildingType::TunnelEntry || it->type == BuildingType::TunnelExit)
|
||
{
|
||
m_belts.removeTile(it->anchor);
|
||
}
|
||
for (const QPoint& cell : it->bodyCells)
|
||
{
|
||
m_tileOccupancy.erase({cell.x(), cell.y()});
|
||
}
|
||
m_buildings.erase(it);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void BuildingSystem::initStationWeapon(EntityId id, const StationWeapon& weapon)
|
||
{
|
||
for (Building& b : m_buildings)
|
||
{
|
||
if (b.id == id)
|
||
{
|
||
b.weapon = weapon;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void BuildingSystem::forEachBuilding(std::function<void(Building&)> fn)
|
||
{
|
||
for (Building& b : m_buildings)
|
||
{
|
||
fn(b);
|
||
}
|
||
}
|