Files
dota_factory/src/lib/sim/BuildingSystem.cpp

949 lines
27 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
}
}