Privatize the Simulation player-action mutators (tryPlaceBuilding, demolish, applySchematicChoice) and the mutable subsystem accessors (now buildingsMutable/ beltsMutable; only the const buildings()/belts() stay public). Production's only handle to a subsystem is through Simulation, and every production buildings()/ belts() call is a const query, so this fully locks gameplay out of any command-bypassing mutation path -- such code now fails to compile instead of silently desyncing replays. Tests reach the private mutators through SimulationTestAccess, a friend struct under src/test (off the lib/ui/app include path), so they keep calling the real mutators and keep their return values -- no id-by-position recovery needed. The BuildingSystem subsystem mutators stay public (BuildingTest unit-tests a bare subsystem; unreachable from production anyway). Upgrades the Phase 1 "convention only" decision to structural enforcement. All targets build; 354 cases / 3418 assertions pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DUsFgd2Ga6pmLz8giS8WUn
518 lines
18 KiB
C++
518 lines
18 KiB
C++
#include "catch.hpp"
|
|
|
|
#include "Building.h"
|
|
#include "BuildingSystem.h"
|
|
#include "BuildingType.h"
|
|
#include "ConfigLoader.h"
|
|
#include "DynamicBodyComponent.h"
|
|
#include "EntityAdmin.h"
|
|
#include "GameConfig.h"
|
|
#include "HealthComponent.h"
|
|
#include "ItemType.h"
|
|
#include "ModuleOwnerComponent.h"
|
|
#include "ModulesConfig.h"
|
|
#include "Rotation.h"
|
|
#include "SensorRangeComponent.h"
|
|
#include "ShipLayout.h"
|
|
#include "ShipStatsCalculator.h"
|
|
#include "ShipSystem.h"
|
|
#include "Simulation.h"
|
|
#include "SimulationTestAccess.h"
|
|
#include "Tick.h"
|
|
#include "WeaponComponent.h"
|
|
|
|
static GameConfig loadConfig()
|
|
{
|
|
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
}
|
|
|
|
static const ShipDef* findSchematic(const GameConfig& cfg, const std::string& id)
|
|
{
|
|
for (const ShipDef& def : cfg.ships.ships)
|
|
{
|
|
if (def.id == id)
|
|
{
|
|
return &def;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static entt::entity findFirstWeaponChild(EntityAdmin& admin, entt::entity ship)
|
|
{
|
|
entt::entity result = entt::null;
|
|
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
|
[&](entt::entity ce, const WeaponComponent&, const ModuleOwnerComponent& o)
|
|
{
|
|
if (o.owner == ship && result == entt::null) { result = ce; }
|
|
});
|
|
return result;
|
|
}
|
|
|
|
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 void fillMaterials(Simulation& sim, BuildingId yardId,
|
|
const ShipDef& def,
|
|
const ShipLayoutConfig& layout)
|
|
{
|
|
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;
|
|
}
|
|
for (const PlacedModule& pm : layout.placedModules)
|
|
{
|
|
for (const ModuleDef& modDef : sim.config().modules.modules)
|
|
{
|
|
if (modDef.id == pm.moduleId)
|
|
{
|
|
for (const RecipeIngredient& ing : modDef.materials)
|
|
{
|
|
b.inputBuffer.counts[ItemType{ing.item}] += ing.amount;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Ship stat modifiers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
|
|
{
|
|
Simulation sim(loadConfig(), 42);
|
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
const float expectedHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
|
|
|
const entt::entity e = sim.ships().spawn("interceptor",
|
|
def->schematic.playerProductionLevel,
|
|
QVector2D(5.0f, 5.0f), false, std::nullopt);
|
|
|
|
REQUIRE(sim.admin().isValid(e));
|
|
CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(expectedHp));
|
|
}
|
|
|
|
TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
|
|
{
|
|
Simulation sim(loadConfig(), 42);
|
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
const float baseHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
|
|
|
ShipLayoutConfig layout;
|
|
PlacedModule pm;
|
|
pm.moduleId = "armor_plate";
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
layout.placedModules.push_back(pm);
|
|
|
|
const entt::entity e = sim.ships().spawn("interceptor",
|
|
def->schematic.playerProductionLevel,
|
|
QVector2D(5.0f, 5.0f), false, layout);
|
|
|
|
REQUIRE(sim.admin().isValid(e));
|
|
// armor_plate has multiplied_hp_formula = "1.5"
|
|
// final = base * (1 + (1.5 - 1)) + 0 = base * 1.5
|
|
CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(baseHp * 1.5f));
|
|
CHECK(sim.admin().get<HealthComponent>(e).hp == sim.admin().get<HealthComponent>(e).maxHp);
|
|
}
|
|
|
|
TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
|
|
{
|
|
Simulation sim(loadConfig(), 42);
|
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
|
const float baseRange_tiles = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x)) / tileSize;
|
|
|
|
ShipLayoutConfig layout;
|
|
PlacedModule pm;
|
|
pm.moduleId = "sensor_booster";
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
layout.placedModules.push_back(pm);
|
|
|
|
const entt::entity e = sim.ships().spawn("interceptor",
|
|
def->schematic.playerProductionLevel,
|
|
QVector2D(5.0f, 5.0f), false, layout);
|
|
|
|
REQUIRE(sim.admin().isValid(e));
|
|
// sensor_booster has added_sensor_range_m_formula = "100" m → 100/10 = 10 tiles
|
|
// final = baseRange_tiles * 1.0 + 10 = baseRange_tiles + 10
|
|
CHECK(sim.admin().get<SensorRangeComponent>(e).value_tiles == Approx(baseRange_tiles + 10.0f));
|
|
}
|
|
|
|
TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
|
|
{
|
|
Simulation sim(loadConfig(), 42);
|
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
const float baseHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
|
|
|
ShipLayoutConfig layout;
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
PlacedModule pm;
|
|
pm.moduleId = "armor_plate";
|
|
pm.position = QPoint(i * 2, 0);
|
|
pm.rotation = Rotation::East;
|
|
layout.placedModules.push_back(pm);
|
|
}
|
|
|
|
const entt::entity e = sim.ships().spawn("interceptor",
|
|
def->schematic.playerProductionLevel,
|
|
QVector2D(5.0f, 5.0f), false, layout);
|
|
|
|
REQUIRE(sim.admin().isValid(e));
|
|
// Two armor_plates: each 1.5 multiplier
|
|
// total_mult = 1 + (1.5 - 1) + (1.5 - 1) = 2.0
|
|
// final = base * 2.0
|
|
CHECK(sim.admin().get<HealthComponent>(e).maxHp == Approx(baseHp * 2.0f));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shipyard module integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
|
|
"[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* b = sim.buildings().findBuilding(yardId);
|
|
REQUIRE(b != nullptr);
|
|
// armor_plate needs 2 iron_ingot; interceptor needs 3 iron_ingot + 1 circuit_board
|
|
// Total iron_ingot = 5, buffer cap = 2 * 5 = 10
|
|
CHECK(b->inputBuffer.caps.at(ItemType{"iron_ingot"}) == 10);
|
|
CHECK(b->inputBuffer.caps.at(ItemType{"circuit_board"}) == 2);
|
|
}
|
|
|
|
TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
|
"[modules][shipyard]")
|
|
{
|
|
Simulation sim(loadConfig(), 42);
|
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
|
REQUIRE(yardDef != nullptr);
|
|
|
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
|
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
|
|
|
// Fill materials and tick to start production.
|
|
ShipLayoutConfig emptyLayout;
|
|
fillMaterials(sim, yardId, *def, emptyLayout);
|
|
sim.tick();
|
|
|
|
const Building* b1 = sim.buildings().findBuilding(yardId);
|
|
REQUIRE(b1 != nullptr);
|
|
REQUIRE(b1->production.has_value());
|
|
|
|
// Now set a layout — should cancel production.
|
|
ShipLayoutConfig layout;
|
|
PlacedModule pm;
|
|
pm.moduleId = "sensor_booster";
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
layout.placedModules.push_back(pm);
|
|
|
|
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
|
|
|
const Building* b2 = sim.buildings().findBuilding(yardId);
|
|
REQUIRE(b2 != nullptr);
|
|
CHECK_FALSE(b2->production.has_value());
|
|
}
|
|
|
|
TEST_CASE("Shipyard: setRecipe clears 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());
|
|
|
|
SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer");
|
|
|
|
const Building* b2 = sim.buildings().findBuilding(yardId);
|
|
REQUIRE(b2 != nullptr);
|
|
CHECK_FALSE(b2->shipLayout.has_value());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Weapon modifier simulation tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[modules]")
|
|
{
|
|
Simulation sim(loadConfig(), 42);
|
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
ShipLayoutConfig layout;
|
|
for (const std::string& id : {"laser_cannon", "weapon_primer"})
|
|
{
|
|
PlacedModule pm;
|
|
pm.moduleId = id;
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
layout.placedModules.push_back(pm);
|
|
}
|
|
|
|
const entt::entity ship = sim.ships().spawn("interceptor",
|
|
def->schematic.playerProductionLevel,
|
|
QVector2D(5.0f, 5.0f), false, layout);
|
|
|
|
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
|
REQUIRE(sim.admin().isValid(weapon));
|
|
// base rate = 2.0 hz; weapon_primer multiplier = 1.2 → 2.4 hz
|
|
CHECK(sim.admin().get<WeaponComponent>(weapon).fireRateHz == Approx(2.4f));
|
|
}
|
|
|
|
TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation", "[modules]")
|
|
{
|
|
Simulation sim(loadConfig(), 42);
|
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
|
|
|
ShipLayoutConfig layout;
|
|
for (const std::string& id : {"laser_cannon", "weapon_stabilizer"})
|
|
{
|
|
PlacedModule pm;
|
|
pm.moduleId = id;
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
layout.placedModules.push_back(pm);
|
|
}
|
|
|
|
const entt::entity ship = sim.ships().spawn("interceptor",
|
|
def->schematic.playerProductionLevel,
|
|
QVector2D(5.0f, 5.0f), false, layout);
|
|
|
|
const entt::entity weapon = findFirstWeaponChild(sim.admin(), ship);
|
|
REQUIRE(sim.admin().isValid(weapon));
|
|
// base range = 50 m / tileSize = 5 tiles; weapon_stabilizer multiplier = 1.5 → 7.5 tiles
|
|
CHECK(sim.admin().get<WeaponComponent>(weapon).range_tiles == Approx(50.0f / tileSize * 1.5f));
|
|
}
|
|
|
|
TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s² to tiles/tick", "[modules]")
|
|
{
|
|
Simulation sim(loadConfig(), 42);
|
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
|
const float tickRate = static_cast<float>(kTickRateHz);
|
|
const float base_mpss = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x));
|
|
|
|
ShipLayoutConfig layout;
|
|
PlacedModule pm;
|
|
pm.moduleId = "afterburner";
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
layout.placedModules.push_back(pm);
|
|
|
|
const entt::entity e = sim.ships().spawn("interceptor",
|
|
def->schematic.playerProductionLevel,
|
|
QVector2D(5.0f, 5.0f), false, layout);
|
|
|
|
// added_main_acceleration_mpss = 60; same conversion as base: / tileSize / tickRate
|
|
const float expected = (base_mpss + 60.0f) / tileSize / tickRate;
|
|
CHECK(sim.admin().get<DynamicBodyComponent>(e).mainAcceleration_tptt == Approx(expected));
|
|
}
|
|
|
|
TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/tick", "[modules]")
|
|
{
|
|
Simulation sim(loadConfig(), 42);
|
|
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
const float tileSize = static_cast<float>(sim.config().world.tileSize_m);
|
|
const float tickRate = static_cast<float>(kTickRateHz);
|
|
const float base_mpss = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x));
|
|
|
|
ShipLayoutConfig layout;
|
|
PlacedModule pm;
|
|
pm.moduleId = "maneuvering_thrusters";
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
layout.placedModules.push_back(pm);
|
|
|
|
const entt::entity e = sim.ships().spawn("interceptor",
|
|
def->schematic.playerProductionLevel,
|
|
QVector2D(5.0f, 5.0f), false, layout);
|
|
|
|
// added_maneuvering_acceleration_mpss = 10; same conversion as base: / tileSize / tickRate
|
|
const float expected = (base_mpss + 10.0f) / tileSize / tickRate;
|
|
CHECK(sim.admin().get<DynamicBodyComponent>(e).maneuveringAcceleration_tptt == Approx(expected));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Weapon modifier stats view tests (calculateShipStats)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("calculateShipStats: weapon_primer multiplies attack rate in stats view", "[modules]")
|
|
{
|
|
const GameConfig cfg = loadConfig();
|
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
std::vector<PlacedModule> modules;
|
|
for (const std::string& id : {"laser_cannon", "weapon_primer"})
|
|
{
|
|
PlacedModule pm;
|
|
pm.moduleId = id;
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
modules.push_back(pm);
|
|
}
|
|
|
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
|
def->schematic.playerProductionLevel, modules);
|
|
|
|
REQUIRE(stats.weapons.has_value());
|
|
// base: damage = 2, rate = 2.0 hz; weapon_primer multiplies rate by 1.2 → DPS = 2 * 2.4 = 4.8
|
|
CHECK(stats.weapons->combinedDps == Approx(4.8f));
|
|
}
|
|
|
|
TEST_CASE("calculateShipStats: weapon_stabilizer multiplies attack range in stats view", "[modules]")
|
|
{
|
|
const GameConfig cfg = loadConfig();
|
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
|
|
|
std::vector<PlacedModule> modules;
|
|
for (const std::string& id : {"laser_cannon", "weapon_stabilizer"})
|
|
{
|
|
PlacedModule pm;
|
|
pm.moduleId = id;
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
modules.push_back(pm);
|
|
}
|
|
|
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
|
def->schematic.playerProductionLevel, modules);
|
|
|
|
REQUIRE(stats.weapons.has_value());
|
|
// base range = 50 m / tileSize; weapon_stabilizer multiplier = 1.5
|
|
CHECK(stats.weapons->maxRange_tiles == Approx(50.0f / tileSize * 1.5f));
|
|
}
|
|
|
|
TEST_CASE("calculateShipStats: afterburner additive main_acceleration is converted m/s² to tiles/s²", "[modules]")
|
|
{
|
|
const GameConfig cfg = loadConfig();
|
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
|
const float base_mpss = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x));
|
|
|
|
std::vector<PlacedModule> modules;
|
|
PlacedModule pm;
|
|
pm.moduleId = "afterburner";
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
modules.push_back(pm);
|
|
|
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
|
def->schematic.playerProductionLevel, modules);
|
|
|
|
// added_main_acceleration_mpss = 60; converted to tiles/s²: / tileSize
|
|
const float expected = (base_mpss + 60.0f) / tileSize;
|
|
CHECK(stats.mainAcceleration_tpss == Approx(expected));
|
|
}
|
|
|
|
TEST_CASE("calculateShipStats: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/s²", "[modules]")
|
|
{
|
|
const GameConfig cfg = loadConfig();
|
|
const ShipDef* def = findSchematic(cfg, "interceptor");
|
|
REQUIRE(def != nullptr);
|
|
|
|
const double x = static_cast<double>(def->schematic.playerProductionLevel);
|
|
const float tileSize = static_cast<float>(cfg.world.tileSize_m);
|
|
const float base_mpss = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x));
|
|
|
|
std::vector<PlacedModule> modules;
|
|
PlacedModule pm;
|
|
pm.moduleId = "maneuvering_thrusters";
|
|
pm.position = QPoint(0, 0);
|
|
pm.rotation = Rotation::East;
|
|
modules.push_back(pm);
|
|
|
|
const ShipStats stats = calculateShipStats(cfg, "interceptor",
|
|
def->schematic.playerProductionLevel, modules);
|
|
|
|
// added_maneuvering_acceleration_mpss = 10; converted to tiles/s²: / tileSize
|
|
const float expected = (base_mpss + 10.0f) / tileSize;
|
|
CHECK(stats.maneuveringAcceleration_tpss == Approx(expected));
|
|
}
|