The eleven forwarding members added last commit are gone; callers now read the data directly through FactoryQueries.h. Simulation and ArenaSimulation expose getFactoryState() so the UI, the balancing view and the tests can reach it. isQueuedForDeconstruction joined the free functions along the way — it only reaches findBuilding, so it was state-pure too. No facade was introduced. The chained form was the reason one looked attractive, but rewriting sim.getBuildings().findBuilding(id) to findBuilding(sim.getFactoryState(), id) turned out to be mechanical, and the result says which data is read rather than which system happens to own it. BuildingSystem.cpp is down to 1735 lines and no longer answers questions about the factory — it only changes it. What remains on it are the mutators, the tick phases, and the queries that also need GameConfig. Verified with a golden-checksum capture before and after — all four sample ticks identical. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
228 lines
6.4 KiB
C++
228 lines
6.4 KiB
C++
#include "catch.hpp"
|
|
#include "FactoryQueries.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "Building.h"
|
|
#include "BuildingSystem.h"
|
|
#include "BuildingType.h"
|
|
#include "ConfigLoader.h"
|
|
#include "EntityAdmin.h"
|
|
#include "FactionComponent.h"
|
|
#include "GameConfig.h"
|
|
#include "ItemType.h"
|
|
#include "Rotation.h"
|
|
#include "ShipIdentityComponent.h"
|
|
#include "ShipSystem.h"
|
|
#include "Simulation.h"
|
|
#include "SimulationTestAccess.h"
|
|
#include "Tick.h"
|
|
#include "TestConfig.h"
|
|
|
|
// A ship starts unlocked iff no unlock group grants it (REQ-LOCK-EXPLICIT).
|
|
static bool startsUnlocked(const GameConfig& cfg, const std::string& shipId)
|
|
{
|
|
for (const UnlockGroupDef& group : cfg.unlocks.groups)
|
|
{
|
|
if (std::find(group.ships.begin(), group.ships.end(), shipId) != group.ships.end())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
|
{
|
|
for (const ShipDef& def : cfg.ships.ships)
|
|
{
|
|
if (startsUnlocked(cfg, def.id) && !def.schematic.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 BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
|
|
{
|
|
return SimulationTestAccess::buildings(sim).placeImmediate(
|
|
BuildingType::Shipyard,
|
|
yardDef.surfaceMask,
|
|
QPoint(0, 0),
|
|
Rotation::East);
|
|
}
|
|
|
|
static int countShips(Simulation& sim)
|
|
{
|
|
int n = 0;
|
|
sim.getAdmin().forEach<ShipIdentityComponent>(
|
|
[&n](entt::entity /*e*/, const ShipIdentityComponent& /*si*/) { ++n; });
|
|
return n;
|
|
}
|
|
|
|
static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def)
|
|
{
|
|
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b)
|
|
{
|
|
if (b.id != yardId)
|
|
{
|
|
return;
|
|
}
|
|
for (const RecipeIngredient& ing : def.schematic.materials)
|
|
{
|
|
b.inputBuffer.counts[ItemType{ing.item}] = ing.amount;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shipyard production
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
|
|
"[shipyard]")
|
|
{
|
|
Simulation sim(loadTestConfig(), 42);
|
|
|
|
const ShipDef* def = findAvailableSchematic(sim.getConfig());
|
|
REQUIRE(def != nullptr);
|
|
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
|
|
REQUIRE(yardDef != nullptr);
|
|
|
|
const int shipsBefore = countShips(sim);
|
|
|
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
|
REQUIRE(yardId != kInvalidBuildingId);
|
|
|
|
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
|
|
fillMaterials(sim, yardId, *def);
|
|
|
|
// First tick: materials consumed, production cycle starts — no ship yet.
|
|
sim.tick();
|
|
REQUIRE(countShips(sim) == shipsBefore);
|
|
|
|
// Tick until the cycle completes.
|
|
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
|
for (Tick i = 1; i < cycleTicks; ++i)
|
|
{
|
|
sim.tick();
|
|
}
|
|
REQUIRE(countShips(sim) == shipsBefore);
|
|
|
|
// Final tick: cycle completes, ship spawns.
|
|
sim.tick();
|
|
REQUIRE(countShips(sim) == shipsBefore + 1);
|
|
|
|
bool foundPlayerShip = false;
|
|
sim.getAdmin().forEach<ShipIdentityComponent, FactionComponent>(
|
|
[&](entt::entity /*e*/, const ShipIdentityComponent& si, const FactionComponent& f)
|
|
{
|
|
if (!f.isEnemy && si.schematicId == def->id)
|
|
{
|
|
foundPlayerShip = true;
|
|
}
|
|
});
|
|
REQUIRE(foundPlayerShip);
|
|
}
|
|
|
|
TEST_CASE("Shipyard: does not spawn without a schematic set", "[shipyard]")
|
|
{
|
|
Simulation sim(loadTestConfig(), 42);
|
|
|
|
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
|
|
REQUIRE(yardDef != nullptr);
|
|
|
|
const int shipsBefore = countShips(sim);
|
|
|
|
placeShipyard(sim, *yardDef);
|
|
|
|
sim.tick();
|
|
|
|
REQUIRE(countShips(sim) == shipsBefore);
|
|
}
|
|
|
|
TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
|
|
{
|
|
Simulation sim(loadTestConfig(), 42);
|
|
|
|
const ShipDef* def = findAvailableSchematic(sim.getConfig());
|
|
REQUIRE(def != nullptr);
|
|
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
|
|
REQUIRE(yardDef != nullptr);
|
|
|
|
const int shipsBefore = countShips(sim);
|
|
|
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
|
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
|
|
// Materials remain at zero (default after setRecipe); no cycle starts.
|
|
|
|
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
|
for (Tick i = 0; i <= cycleTicks; ++i)
|
|
{
|
|
sim.tick();
|
|
}
|
|
|
|
REQUIRE(countShips(sim) == shipsBefore);
|
|
}
|
|
|
|
TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipyard]")
|
|
{
|
|
Simulation sim(loadTestConfig(), 42);
|
|
|
|
const ShipDef* def = findAvailableSchematic(sim.getConfig());
|
|
REQUIRE(def != nullptr);
|
|
const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
|
|
REQUIRE(yardDef != nullptr);
|
|
|
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
|
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
|
|
|
|
const Tick cycleTicks = secondsToTicks(def->schematic.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 = countShips(sim);
|
|
|
|
// 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 = countShips(sim);
|
|
|
|
// 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 : getAllBuildings(sim.getFactoryState()))
|
|
{
|
|
if (b.id == yardId)
|
|
{
|
|
productionCleared = !b.production.has_value();
|
|
break;
|
|
}
|
|
}
|
|
REQUIRE(productionCleared);
|
|
}
|