Compare commits
2 Commits
4986c1bac8
...
cd966daba9
| Author | SHA1 | Date | |
|---|---|---|---|
| cd966daba9 | |||
| 88bc4f2170 |
@@ -419,6 +419,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
|||||||
{
|
{
|
||||||
if (site.id == id)
|
if (site.id == id)
|
||||||
{
|
{
|
||||||
|
// No-op if the recipe is unchanged, so a redundant selection does
|
||||||
|
// not wipe an already-configured ship layout.
|
||||||
|
if (site.recipeId == recipeId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
site.recipeId = recipeId;
|
site.recipeId = recipeId;
|
||||||
site.shipLayout = std::nullopt;
|
site.shipLayout = std::nullopt;
|
||||||
return;
|
return;
|
||||||
@@ -430,6 +436,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
|||||||
{
|
{
|
||||||
if (building.id == id)
|
if (building.id == id)
|
||||||
{
|
{
|
||||||
|
// No-op if the recipe is unchanged, so a redundant selection does
|
||||||
|
// not wipe an already-configured ship layout or reset buffers.
|
||||||
|
if (building.recipeId == recipeId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
building.recipeId = recipeId;
|
building.recipeId = recipeId;
|
||||||
building.shipLayout = std::nullopt;
|
building.shipLayout = std::nullopt;
|
||||||
building.inputBuffer.counts.clear();
|
building.inputBuffer.counts.clear();
|
||||||
@@ -858,7 +870,16 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
|||||||
{
|
{
|
||||||
const Port& p = building.outputPorts[0];
|
const Port& p = building.outputPorts[0];
|
||||||
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
||||||
m_spawnShip(building.recipeId, spawnPos, building.shipLayout);
|
// A shipyard builds exactly what the player configured and
|
||||||
|
// paid for. When no layout is set it produces a bare hull, so
|
||||||
|
// pass an explicit empty layout rather than nullopt: the latter
|
||||||
|
// would make ShipSystem fall back to the schematic's
|
||||||
|
// defaultModules (a wave-only loadout) and yield free weapons.
|
||||||
|
const std::optional<ShipLayoutConfig> layout =
|
||||||
|
building.shipLayout.has_value()
|
||||||
|
? building.shipLayout
|
||||||
|
: std::make_optional<ShipLayoutConfig>();
|
||||||
|
m_spawnShip(building.recipeId, spawnPos, layout);
|
||||||
}
|
}
|
||||||
building.production = std::nullopt;
|
building.production = std::nullopt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
|
#include "FactionComponent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "ItemType.h"
|
#include "ItemType.h"
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -264,6 +266,50 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
|||||||
CHECK_FALSE(b2->production.has_value());
|
CHECK_FALSE(b2->production.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
|
||||||
|
"[modules][shipyard]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||||
|
REQUIRE(def != nullptr);
|
||||||
|
// The schematic carries a weapon in its (wave-only) default loadout. This
|
||||||
|
// test pins that a player shipyard with no configured layout does NOT hand
|
||||||
|
// that weapon out for free: it builds an unarmed bare hull, matching the
|
||||||
|
// base-hull materials it was charged.
|
||||||
|
REQUIRE_FALSE(def->defaultModules.empty());
|
||||||
|
|
||||||
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||||
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
|
SimulationTestAccess::buildings(sim).setRecipe(yardId, "interceptor");
|
||||||
|
// Deliberately no setShipLayout: recipe set, layout left unconfigured.
|
||||||
|
|
||||||
|
// Charge only the base-hull materials (an empty layout adds none).
|
||||||
|
fillMaterials(sim, yardId, *def, ShipLayoutConfig{});
|
||||||
|
|
||||||
|
// Tick through one full production cycle so the ship spawns.
|
||||||
|
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
||||||
|
for (Tick i = 0; i <= cycleTicks; ++i)
|
||||||
|
{
|
||||||
|
sim.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate the freshly built player ship.
|
||||||
|
entt::entity built = entt::null;
|
||||||
|
sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
|
||||||
|
[&](entt::entity e, const ShipIdentityComponent& si, const FactionComponent& fac)
|
||||||
|
{
|
||||||
|
if (!fac.isEnemy && si.schematicId == "interceptor") { built = e; }
|
||||||
|
});
|
||||||
|
REQUIRE(sim.admin().isValid(built));
|
||||||
|
|
||||||
|
// Bare hull: the schematic's default weapon must NOT have been installed.
|
||||||
|
const bool hasWeapon =
|
||||||
|
findFirstWeaponChild(sim.admin(), built) != entt::null;
|
||||||
|
CHECK_FALSE(hasWeapon);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadConfig(), 42);
|
||||||
@@ -292,6 +338,38 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
|||||||
CHECK_FALSE(b2->shipLayout.has_value());
|
CHECK_FALSE(b2->shipLayout.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
|
||||||
|
"[modules][shipyard]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||||
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
|
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
||||||
|
|
||||||
|
ShipLayoutConfig layout;
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = "armor_plate";
|
||||||
|
pm.position = QPoint(0, 0);
|
||||||
|
pm.rotation = Rotation::East;
|
||||||
|
layout.placedModules.push_back(pm);
|
||||||
|
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
||||||
|
|
||||||
|
const Building* b1 = sim.buildings().findBuilding(yardId);
|
||||||
|
REQUIRE(b1 != nullptr);
|
||||||
|
REQUIRE(b1->shipLayout.has_value());
|
||||||
|
|
||||||
|
// Re-selecting the same recipe must be a no-op and preserve the layout.
|
||||||
|
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
||||||
|
|
||||||
|
const Building* b2 = sim.buildings().findBuilding(yardId);
|
||||||
|
REQUIRE(b2 != nullptr);
|
||||||
|
REQUIRE(b2->shipLayout.has_value());
|
||||||
|
REQUIRE(b2->shipLayout->placedModules.size() == 1);
|
||||||
|
CHECK(b2->shipLayout->placedModules[0].moduleId == "armor_plate");
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Weapon modifier simulation tests
|
// Weapon modifier simulation tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user