implement ship modules
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
#include "ItemType.h"
|
||||
#include "Port.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "Tick.h"
|
||||
|
||||
// Per-material input buffer for a production building.
|
||||
@@ -50,6 +51,7 @@ struct ConstructionSite
|
||||
BuildingType type = BuildingType::Miner;
|
||||
std::string recipeId; // may be configured before completion
|
||||
Tick completesAt = 0; // 0 = queued but not yet started
|
||||
std::optional<ShipLayoutConfig> shipLayout;
|
||||
};
|
||||
|
||||
// Weapon state for stationary structures (defence stations).
|
||||
@@ -85,6 +87,9 @@ struct Building
|
||||
std::vector<Port> inputPorts; // perimeter tiles (minus output-port tiles),
|
||||
// direction pointing INTO building
|
||||
|
||||
// Module layout for shipyards (REQ-MOD-LAYOUT).
|
||||
std::optional<ShipLayoutConfig> shipLayout;
|
||||
|
||||
// Set only for defence stations; nullopt for all other building types.
|
||||
std::optional<StationWeapon> weapon;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,8 @@ 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::function<void(const std::string&, QVector2D,
|
||||
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
||||
std::mt19937& rng)
|
||||
: m_config(config)
|
||||
, m_belts(belts)
|
||||
@@ -63,6 +64,18 @@ const ShipDef* BuildingSystem::findShipDef(const std::string& id) const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ModuleDef* BuildingSystem::findModuleDef(const std::string& id) const
|
||||
{
|
||||
for (const ModuleDef& def : m_config.modules.modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
@@ -117,6 +130,23 @@ void BuildingSystem::initShipyardBuffers(Building& b) const
|
||||
b.inputBuffer.counts[type] = 0;
|
||||
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||
}
|
||||
if (b.shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const RecipeIngredient& ing : modDef->materials)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts.try_emplace(type, 0);
|
||||
b.inputBuffer.caps[type] += 2 * ing.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Port> BuildingSystem::computeInputPorts(const Building& b) const
|
||||
@@ -303,6 +333,7 @@ void BuildingSystem::setRecipe(EntityId id, const std::string& recipeId)
|
||||
if (site.id == id)
|
||||
{
|
||||
site.recipeId = recipeId;
|
||||
site.shipLayout = std::nullopt;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -313,6 +344,7 @@ void BuildingSystem::setRecipe(EntityId id, const std::string& recipeId)
|
||||
if (building.id == id)
|
||||
{
|
||||
building.recipeId = recipeId;
|
||||
building.shipLayout = std::nullopt;
|
||||
building.inputBuffer.counts.clear();
|
||||
building.inputBuffer.caps.clear();
|
||||
building.outputBuffer.items.clear();
|
||||
@@ -339,6 +371,39 @@ void BuildingSystem::setRecipe(EntityId id, const std::string& recipeId)
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::setShipLayout(EntityId id, const ShipLayoutConfig& layout)
|
||||
{
|
||||
for (ConstructionSite& site : m_constructionQueue)
|
||||
{
|
||||
if (site.id == id)
|
||||
{
|
||||
site.shipLayout = layout;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (Building& building : m_buildings)
|
||||
{
|
||||
if (building.id == id)
|
||||
{
|
||||
if (building.production.has_value())
|
||||
{
|
||||
building.production = std::nullopt;
|
||||
}
|
||||
building.shipLayout = layout;
|
||||
building.inputBuffer.counts.clear();
|
||||
building.inputBuffer.caps.clear();
|
||||
building.outputBuffer.items.clear();
|
||||
building.outputBuffer.capacity = 0;
|
||||
if (!building.recipeId.empty() && building.type == BuildingType::Shipyard)
|
||||
{
|
||||
initShipyardBuffers(building);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tick hooks
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -383,6 +448,7 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
building.hp = 100.0f;
|
||||
building.maxHp = 100.0f;
|
||||
building.recipeId = front.recipeId;
|
||||
building.shipLayout = front.shipLayout;
|
||||
|
||||
for (const QPoint& cell : mask.bodyCells)
|
||||
{
|
||||
@@ -657,22 +723,44 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
{
|
||||
const Port& p = building.outputPorts[0];
|
||||
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
||||
m_spawnShip(building.recipeId, spawnPos);
|
||||
m_spawnShip(building.recipeId, spawnPos, building.shipLayout);
|
||||
}
|
||||
building.production = std::nullopt;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Idle: check if all materials are available to start a new cycle.
|
||||
bool inputsOk = true;
|
||||
// Build combined materials list (base + modules).
|
||||
std::map<std::string, int> requiredMaterials;
|
||||
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
requiredMaterials[ing.item] += ing.amount;
|
||||
}
|
||||
if (building.shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : building.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const RecipeIngredient& ing : modDef->materials)
|
||||
{
|
||||
requiredMaterials[ing.item] += ing.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Idle: check if all combined materials are available.
|
||||
bool inputsOk = true;
|
||||
for (const std::pair<const std::string, int>& req : requiredMaterials)
|
||||
{
|
||||
const ItemType type{req.first};
|
||||
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)
|
||||
if (have < req.second)
|
||||
{
|
||||
inputsOk = false;
|
||||
break;
|
||||
@@ -683,16 +771,28 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Consume materials and start the production cycle.
|
||||
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
||||
// Consume combined materials and start the production cycle.
|
||||
for (const std::pair<const std::string, int>& req : requiredMaterials)
|
||||
{
|
||||
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
|
||||
building.inputBuffer.counts[ItemType{req.first}] -= req.second;
|
||||
}
|
||||
|
||||
double totalTime = shipDef->schematic.productionTimeSeconds;
|
||||
if (building.shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : building.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (modDef)
|
||||
{
|
||||
totalTime += modDef->productionTimeSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Production prod;
|
||||
prod.recipeId = building.recipeId;
|
||||
prod.completesAt = currentTick
|
||||
+ secondsToTicks(shipDef->schematic.productionTimeSeconds);
|
||||
prod.completesAt = currentTick + secondsToTicks(totalTime);
|
||||
building.production = std::move(prod);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include "EntityId.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Rotation.h"
|
||||
#include "ModulesConfig.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipsConfig.h"
|
||||
#include "Tick.h"
|
||||
|
||||
@@ -32,7 +34,8 @@ public:
|
||||
BeltSystem& belts,
|
||||
std::function<EntityId()> allocateId,
|
||||
std::function<void(int)> addBuildingBlocks,
|
||||
std::function<void(const std::string&, QVector2D)> spawnShip,
|
||||
std::function<void(const std::string&, QVector2D,
|
||||
const std::optional<ShipLayoutConfig>&)> spawnShip,
|
||||
std::mt19937& rng);
|
||||
|
||||
// -- Placement / demolish ------------------------------------------------
|
||||
@@ -50,6 +53,10 @@ public:
|
||||
// construction site. Clears both buffers on an operational building.
|
||||
void setRecipe(EntityId 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(EntityId id, const ShipLayoutConfig& layout);
|
||||
|
||||
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
||||
void tickConstruction(Tick currentTick);
|
||||
void tickBeltPull();
|
||||
@@ -121,6 +128,7 @@ private:
|
||||
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;
|
||||
void initShipyardBuffers(Building& b) const;
|
||||
std::vector<Port> computeInputPorts(const Building& b) const;
|
||||
@@ -130,7 +138,8 @@ private:
|
||||
BeltSystem& m_belts;
|
||||
std::function<EntityId()> m_allocateId;
|
||||
std::function<void(int)> m_addBuildingBlocks;
|
||||
std::function<void(const std::string&, QVector2D)> m_spawnShip;
|
||||
std::function<void(const std::string&, QVector2D,
|
||||
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
||||
std::mt19937& m_rng;
|
||||
|
||||
std::vector<Building> m_buildings;
|
||||
|
||||
@@ -7,6 +7,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Scrap.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Ship.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ScrapSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
||||
|
||||
22
src/lib/sim/ShipLayout.h
Normal file
22
src/lib/sim/ShipLayout.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
|
||||
#include "Rotation.h"
|
||||
|
||||
// A single module placed on a ship's layout grid (REQ-MOD-PLACEMENT).
|
||||
struct PlacedModule
|
||||
{
|
||||
std::string moduleId;
|
||||
QPoint position;
|
||||
Rotation rotation;
|
||||
};
|
||||
|
||||
// The complete module configuration for a shipyard's current ship (REQ-MOD-CONFIG).
|
||||
struct ShipLayoutConfig
|
||||
{
|
||||
std::vector<PlacedModule> placedModules;
|
||||
};
|
||||
@@ -3,10 +3,13 @@
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ModulesConfig.h"
|
||||
#include "Scrap.h"
|
||||
#include "ScrapSystem.h"
|
||||
#include "Tick.h"
|
||||
@@ -30,8 +33,21 @@ const ShipDef* ShipSystem::findShipDef(const std::string& schematicId) const
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
|
||||
{
|
||||
for (const ModuleDef& def : m_config.modules.modules)
|
||||
{
|
||||
if (def.id == id)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D position,
|
||||
bool isEnemy)
|
||||
bool isEnemy,
|
||||
const std::optional<ShipLayoutConfig>& layout)
|
||||
{
|
||||
const ShipDef* def = findShipDef(schematicId);
|
||||
assert(def != nullptr);
|
||||
@@ -95,6 +111,60 @@ EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D
|
||||
ship.repairBehavior = rb;
|
||||
}
|
||||
|
||||
// Apply module stat modifiers (REQ-MOD-STAT-CALC).
|
||||
if (layout.has_value() && !layout->placedModules.empty())
|
||||
{
|
||||
std::map<std::string, std::pair<double, double>> mods;
|
||||
for (const PlacedModule& pm : layout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
||||
{
|
||||
const double val = sm.formula.evaluate(
|
||||
static_cast<double>(modDef->playerProductionLevel));
|
||||
std::pair<double, double>& acc = mods[sm.stat];
|
||||
if (sm.modifierType == "multiplicative")
|
||||
{
|
||||
acc.first += (val - 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
acc.second += val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto applyMod = [&mods](float& stat, const std::string& name) {
|
||||
const std::map<std::string, std::pair<double, double>>::const_iterator it =
|
||||
mods.find(name);
|
||||
if (it != mods.end())
|
||||
{
|
||||
stat = static_cast<float>(
|
||||
static_cast<double>(stat) * (1.0 + it->second.first) + it->second.second);
|
||||
}
|
||||
};
|
||||
|
||||
applyMod(ship.maxHp, "hp");
|
||||
ship.hp = ship.maxHp;
|
||||
applyMod(ship.speedPerTick, "speed");
|
||||
applyMod(ship.sensorRange, "sensor_range");
|
||||
if (ship.weapon.has_value())
|
||||
{
|
||||
applyMod(ship.weapon->damage, "damage");
|
||||
applyMod(ship.weapon->range, "attack_range");
|
||||
applyMod(ship.weapon->fireRateHz, "attack_rate");
|
||||
}
|
||||
if (ship.repairTool.has_value())
|
||||
{
|
||||
applyMod(ship.repairTool->ratePerTick, "repair_rate");
|
||||
applyMod(ship.repairTool->range, "repair_range");
|
||||
}
|
||||
}
|
||||
|
||||
m_ships.push_back(ship);
|
||||
return ship.id;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "EntityId.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Ship.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
class BuildingSystem;
|
||||
class ScrapSystem;
|
||||
@@ -20,7 +21,8 @@ public:
|
||||
|
||||
// isEnemy defaults to false; set true for enemy-faction ships (step 7 wave spawning).
|
||||
EntityId spawn(const std::string& schematicId, int level, QVector2D position,
|
||||
bool isEnemy = false);
|
||||
bool isEnemy = false,
|
||||
const std::optional<ShipLayoutConfig>& layout = std::nullopt);
|
||||
void despawn(EntityId id);
|
||||
|
||||
const Ship* findShip(EntityId id) const;
|
||||
@@ -58,7 +60,8 @@ public:
|
||||
bool damageShip(EntityId id, float amount);
|
||||
|
||||
private:
|
||||
const ShipDef* findShipDef(const std::string& schematicId) const;
|
||||
const ShipDef* findShipDef(const std::string& schematicId) const;
|
||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||
|
||||
// True if the entity identified by id is alive and within range of ship.
|
||||
// Searches both the ship list and (for buildings) the supplied BuildingSystem.
|
||||
|
||||
@@ -30,14 +30,15 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
||||
m_beltSystem,
|
||||
[this]() { return allocateId(); },
|
||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||
[this](const std::string& id, QVector2D pos) {
|
||||
[this](const std::string& id, QVector2D pos,
|
||||
const std::optional<ShipLayoutConfig>& layout) {
|
||||
const std::map<std::string, SchematicState>::const_iterator it =
|
||||
m_schematicLevels.find(id);
|
||||
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false);
|
||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
|
||||
},
|
||||
m_rng);
|
||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
||||
@@ -92,14 +93,15 @@ void Simulation::reset(unsigned int seed)
|
||||
m_beltSystem,
|
||||
[this]() { return allocateId(); },
|
||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||
[this](const std::string& id, QVector2D pos) {
|
||||
[this](const std::string& id, QVector2D pos,
|
||||
const std::optional<ShipLayoutConfig>& layout) {
|
||||
const std::map<std::string, SchematicState>::const_iterator it =
|
||||
m_schematicLevels.find(id);
|
||||
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false);
|
||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false, layout);
|
||||
},
|
||||
m_rng);
|
||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
||||
|
||||
Reference in New Issue
Block a user