fix issue where shipyard did not produce anything
This commit is contained in:
@@ -367,6 +367,8 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||
def.blueprint.materials = parseIngredients(materials, file, bpPath + ".materials");
|
||||
def.blueprint.playerProductionLevel = static_cast<int>(requireInt(
|
||||
bpMt["player_production_level"], file, bpPath + ".player_production_level"));
|
||||
def.blueprint.productionTimeSeconds = requireDouble(
|
||||
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
||||
}
|
||||
|
||||
// Threat
|
||||
|
||||
@@ -13,6 +13,7 @@ struct ShipBlueprint
|
||||
{
|
||||
std::vector<RecipeIngredient> materials;
|
||||
int playerProductionLevel;
|
||||
double productionTimeSeconds;
|
||||
};
|
||||
|
||||
// Wave scheduling cost (REQ-WAV-THREAT-COST). Ships with cost_formula that
|
||||
|
||||
@@ -11,11 +11,13 @@ 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)
|
||||
{
|
||||
}
|
||||
@@ -49,6 +51,18 @@ const RecipeDef* BuildingSystem::findRecipe(const std::string& id,
|
||||
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();
|
||||
@@ -86,6 +100,25 @@ void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
|
||||
}
|
||||
}
|
||||
|
||||
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->blueprint.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.
|
||||
@@ -323,10 +356,17 @@ void BuildingSystem::setRecipe(EntityId id, const std::string& recipeId)
|
||||
|
||||
if (!recipeId.empty())
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(recipeId, building.type);
|
||||
if (recipe)
|
||||
if (building.type == BuildingType::Shipyard)
|
||||
{
|
||||
initBuffers(building, *recipe);
|
||||
initShipyardBuffers(building);
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(recipeId, building.type);
|
||||
if (recipe)
|
||||
{
|
||||
initBuffers(building, *recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -394,10 +434,17 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
|
||||
if (!building.recipeId.empty())
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (recipe)
|
||||
if (building.type == BuildingType::Shipyard)
|
||||
{
|
||||
initBuffers(building, *recipe);
|
||||
initShipyardBuffers(building);
|
||||
}
|
||||
else
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (recipe)
|
||||
{
|
||||
initBuffers(building, *recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,10 +490,13 @@ void BuildingSystem::tickBeltPull()
|
||||
continue;
|
||||
}
|
||||
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (!recipe || recipe->inputs.empty())
|
||||
if (building.type != BuildingType::Shipyard)
|
||||
{
|
||||
continue;
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (!recipe || recipe->inputs.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const Port& port : building.inputPorts)
|
||||
@@ -593,6 +643,73 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
||||
}
|
||||
}
|
||||
|
||||
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->blueprint.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->blueprint.materials)
|
||||
{
|
||||
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
|
||||
}
|
||||
|
||||
Production prod;
|
||||
prod.recipeId = building.recipeId;
|
||||
prod.completesAt = currentTick
|
||||
+ secondsToTicks(shipDef->blueprint.productionTimeSeconds);
|
||||
building.production = std::move(prod);
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::tickBeltPush()
|
||||
{
|
||||
for (Building& building : m_buildings)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "EntityId.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipsConfig.h"
|
||||
#include "Tick.h"
|
||||
|
||||
// Manages building placement, construction queuing, and the per-tick
|
||||
@@ -31,6 +32,7 @@ public:
|
||||
BeltSystem& belts,
|
||||
std::function<EntityId()> allocateId,
|
||||
std::function<void(int)> addBuildingBlocks,
|
||||
std::function<void(const std::string&, QVector2D)> spawnShip,
|
||||
std::mt19937& rng);
|
||||
|
||||
// -- Placement / demolish ------------------------------------------------
|
||||
@@ -52,6 +54,7 @@ public:
|
||||
void tickConstruction(Tick currentTick);
|
||||
void tickBeltPull();
|
||||
void tickProduction(Tick currentTick);
|
||||
void tickShipyardProduction(Tick currentTick);
|
||||
void tickBeltPush();
|
||||
|
||||
// -- Queries -------------------------------------------------------------
|
||||
@@ -113,15 +116,18 @@ 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;
|
||||
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
||||
void initShipyardBuffers(Building& b) 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;
|
||||
const GameConfig& m_config;
|
||||
BeltSystem& m_belts;
|
||||
std::function<EntityId()> m_allocateId;
|
||||
std::function<void(int)> m_addBuildingBlocks;
|
||||
std::function<void(const std::string&, QVector2D)> m_spawnShip;
|
||||
std::mt19937& m_rng;
|
||||
|
||||
std::vector<Building> m_buildings;
|
||||
std::deque<ConstructionSite> m_constructionQueue;
|
||||
|
||||
@@ -29,6 +29,15 @@ Simulation::Simulation(const GameConfig& config, unsigned int seed)
|
||||
m_beltSystem,
|
||||
[this]() { return allocateId(); },
|
||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||
[this](const std::string& id, QVector2D pos) {
|
||||
const std::map<std::string, BlueprintState>::const_iterator it =
|
||||
m_blueprintLevels.find(id);
|
||||
if (it == m_blueprintLevels.end() || !it->second.unlocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false);
|
||||
},
|
||||
m_rng);
|
||||
m_shipSystem = std::make_unique<ShipSystem>(config, [this]() { return allocateId(); });
|
||||
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
|
||||
@@ -70,6 +79,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) {
|
||||
const std::map<std::string, BlueprintState>::const_iterator it =
|
||||
m_blueprintLevels.find(id);
|
||||
if (it == m_blueprintLevels.end() || !it->second.unlocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_shipSystem->spawn(id, it->second.level, pos, /*isEnemy=*/false);
|
||||
},
|
||||
m_rng);
|
||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, [this]() { return allocateId(); });
|
||||
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
|
||||
@@ -105,6 +123,7 @@ void Simulation::tick()
|
||||
m_buildingSystem->tickConstruction(m_currentTick);
|
||||
m_buildingSystem->tickBeltPull(); // step 3
|
||||
m_buildingSystem->tickProduction(m_currentTick); // step 4
|
||||
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b
|
||||
m_buildingSystem->tickBeltPush(); // step 5
|
||||
m_beltSystem.tick(); // step 6
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ struct Fixture
|
||||
, buildings(cfg, belts,
|
||||
[this]() { return nextId++; },
|
||||
[this](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng)
|
||||
, ships(cfg, [this]() { return nextId++; })
|
||||
, scraps([this]() { return nextId++; })
|
||||
|
||||
@@ -78,6 +78,7 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -101,6 +102,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem", "[build
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0);
|
||||
@@ -119,6 +121,7 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -138,6 +141,7 @@ TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -164,6 +168,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -180,6 +185,7 @@ TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[b
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -200,6 +206,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -223,6 +230,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -251,6 +259,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -279,6 +288,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -317,6 +327,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
// Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1).
|
||||
@@ -356,6 +367,7 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -394,6 +406,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||
@@ -433,6 +446,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::ReprocessingPlant,
|
||||
@@ -462,6 +476,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextId]() { return nextId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId id = bs.place(BuildingType::ReprocessingPlant,
|
||||
|
||||
@@ -14,4 +14,5 @@ add_files(
|
||||
BehaviorSystemTest.cpp
|
||||
WaveSystemTest.cpp
|
||||
CombatSystemTest.cpp
|
||||
ShipyardTest.cpp
|
||||
)
|
||||
|
||||
@@ -52,6 +52,7 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
// Spawn an enemy combat ship close to the player side.
|
||||
@@ -112,6 +113,7 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(5.0f, 5.0f), true);
|
||||
@@ -160,6 +162,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
||||
BuildingSystem buildings(cfg, belts,
|
||||
[&nextBldId]() { return nextBldId++; },
|
||||
[](int){},
|
||||
[](const std::string&, QVector2D) {},
|
||||
rng);
|
||||
|
||||
const EntityId enemyId = ships.spawn(combatDef->id, 1, QVector2D(0.0f, 0.0f), true);
|
||||
|
||||
209
src/test/ShipyardTest.cpp
Normal file
209
src/test/ShipyardTest.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ConfigLoader.h"
|
||||
#include "GameConfig.h"
|
||||
#include "ItemType.h"
|
||||
#include "Rotation.h"
|
||||
#include "Ship.h"
|
||||
#include "ShipSystem.h"
|
||||
#include "Simulation.h"
|
||||
#include "Tick.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(DOTA_FACTORY_CONFIG_DIR);
|
||||
}
|
||||
|
||||
static const ShipDef* findAvailableBlueprint(const GameConfig& cfg)
|
||||
{
|
||||
for (const ShipDef& def : cfg.ships.ships)
|
||||
{
|
||||
if (def.availableFromStart && !def.blueprint.materials.empty())
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const BuildingDef* findShipyardDef(const GameConfig& cfg)
|
||||
{
|
||||
for (const BuildingDef& def : cfg.buildings.buildings)
|
||||
{
|
||||
if (def.type == BuildingType::Shipyard)
|
||||
{
|
||||
return &def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static EntityId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
|
||||
{
|
||||
return sim.buildings().placeImmediate(
|
||||
BuildingType::Shipyard,
|
||||
yardDef.surfaceMask,
|
||||
QPoint(0, 0),
|
||||
Rotation::East,
|
||||
100.0f, 100.0f);
|
||||
}
|
||||
|
||||
static void fillMaterials(Simulation& sim, EntityId yardId, const ShipDef& def)
|
||||
{
|
||||
sim.buildings().forEachBuilding([&](Building& b)
|
||||
{
|
||||
if (b.id != yardId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (const RecipeIngredient& ing : def.blueprint.materials)
|
||||
{
|
||||
b.inputBuffer.counts[ItemType{ing.item}] = ing.amount;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shipyard production
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
|
||||
"[shipyard]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
const ShipDef* def = findAvailableBlueprint(cfg);
|
||||
REQUIRE(def != nullptr);
|
||||
const BuildingDef* yardDef = findShipyardDef(cfg);
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const int shipsBefore = static_cast<int>(sim.ships().allShips().size());
|
||||
|
||||
const EntityId yardId = placeShipyard(sim, *yardDef);
|
||||
REQUIRE(yardId != kInvalidEntityId);
|
||||
|
||||
sim.buildings().setRecipe(yardId, def->id);
|
||||
fillMaterials(sim, yardId, *def);
|
||||
|
||||
// First tick: materials consumed, production cycle starts — no ship yet.
|
||||
sim.tick();
|
||||
REQUIRE(static_cast<int>(sim.ships().allShips().size()) == shipsBefore);
|
||||
|
||||
// Tick until the cycle completes.
|
||||
const Tick cycleTicks = secondsToTicks(def->blueprint.productionTimeSeconds);
|
||||
for (Tick i = 1; i < cycleTicks; ++i)
|
||||
{
|
||||
sim.tick();
|
||||
}
|
||||
REQUIRE(static_cast<int>(sim.ships().allShips().size()) == shipsBefore);
|
||||
|
||||
// Final tick: cycle completes, ship spawns.
|
||||
sim.tick();
|
||||
REQUIRE(static_cast<int>(sim.ships().allShips().size()) == shipsBefore + 1);
|
||||
|
||||
bool foundPlayerShip = false;
|
||||
for (const Ship& ship : sim.ships().allShips())
|
||||
{
|
||||
if (!ship.isEnemy && ship.blueprintId == def->id)
|
||||
{
|
||||
foundPlayerShip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(foundPlayerShip);
|
||||
}
|
||||
|
||||
TEST_CASE("Shipyard: does not spawn without a blueprint set", "[shipyard]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
const BuildingDef* yardDef = findShipyardDef(cfg);
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const int shipsBefore = static_cast<int>(sim.ships().allShips().size());
|
||||
|
||||
placeShipyard(sim, *yardDef);
|
||||
|
||||
sim.tick();
|
||||
|
||||
REQUIRE(static_cast<int>(sim.ships().allShips().size()) == shipsBefore);
|
||||
}
|
||||
|
||||
TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
const ShipDef* def = findAvailableBlueprint(cfg);
|
||||
REQUIRE(def != nullptr);
|
||||
const BuildingDef* yardDef = findShipyardDef(cfg);
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const int shipsBefore = static_cast<int>(sim.ships().allShips().size());
|
||||
|
||||
const EntityId yardId = placeShipyard(sim, *yardDef);
|
||||
sim.buildings().setRecipe(yardId, def->id);
|
||||
// Materials remain at zero (default after setRecipe); no cycle starts.
|
||||
|
||||
const Tick cycleTicks = secondsToTicks(def->blueprint.productionTimeSeconds);
|
||||
for (Tick i = 0; i <= cycleTicks; ++i)
|
||||
{
|
||||
sim.tick();
|
||||
}
|
||||
|
||||
REQUIRE(static_cast<int>(sim.ships().allShips().size()) == shipsBefore);
|
||||
}
|
||||
|
||||
TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipyard]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(cfg, 42);
|
||||
|
||||
const ShipDef* def = findAvailableBlueprint(cfg);
|
||||
REQUIRE(def != nullptr);
|
||||
const BuildingDef* yardDef = findShipyardDef(cfg);
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const EntityId yardId = placeShipyard(sim, *yardDef);
|
||||
sim.buildings().setRecipe(yardId, def->id);
|
||||
|
||||
const Tick cycleTicks = secondsToTicks(def->blueprint.productionTimeSeconds);
|
||||
|
||||
// First cycle: capture count immediately after the spawn tick.
|
||||
fillMaterials(sim, yardId, *def);
|
||||
for (Tick i = 0; i <= cycleTicks; ++i)
|
||||
{
|
||||
sim.tick();
|
||||
}
|
||||
const int after1 = static_cast<int>(sim.ships().allShips().size());
|
||||
|
||||
// Second cycle: capture count immediately after the next spawn tick.
|
||||
fillMaterials(sim, yardId, *def);
|
||||
for (Tick i = 0; i <= cycleTicks; ++i)
|
||||
{
|
||||
sim.tick();
|
||||
}
|
||||
const int after2 = static_cast<int>(sim.ships().allShips().size());
|
||||
|
||||
// After each cycle one ship was added; ships from prior cycles may have died
|
||||
// from enemy fire, so we only assert the most-recent spawn is still present.
|
||||
REQUIRE(after2 >= 1);
|
||||
|
||||
// Verify the shipyard production field cleared (i.e. the cycle completed
|
||||
// and is not still running).
|
||||
bool productionCleared = false;
|
||||
for (const Building& b : sim.buildings().allBuildings())
|
||||
{
|
||||
if (b.id == yardId)
|
||||
{
|
||||
productionCleared = !b.production.has_value();
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(productionCleared);
|
||||
}
|
||||
Reference in New Issue
Block a user