fix issue where shipyard did not produce anything
This commit is contained in:
@@ -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