diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 87d11c0..3ef9c2c 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -870,7 +870,16 @@ 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, 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 layout = + building.shipLayout.has_value() + ? building.shipLayout + : std::make_optional(); + m_spawnShip(building.recipeId, spawnPos, layout); } building.production = std::nullopt; } diff --git a/src/test/ShipModuleTest.cpp b/src/test/ShipModuleTest.cpp index 5ffcf9f..40a6ffe 100644 --- a/src/test/ShipModuleTest.cpp +++ b/src/test/ShipModuleTest.cpp @@ -6,6 +6,7 @@ #include "ConfigLoader.h" #include "DynamicBodyComponent.h" #include "EntityAdmin.h" +#include "FactionComponent.h" #include "GameConfig.h" #include "HealthComponent.h" #include "ItemType.h" @@ -13,6 +14,7 @@ #include "ModulesConfig.h" #include "Rotation.h" #include "SensorRangeComponent.h" +#include "ShipIdentityComponent.h" #include "ShipLayout.h" #include "ShipStatsCalculator.h" #include "ShipSystem.h" @@ -264,6 +266,50 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production", 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( + [&](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]") { Simulation sim(loadConfig(), 42);