fix issue where upgrade modules are not working properly
This commit is contained in:
@@ -8,6 +8,24 @@ static GameConfig loadConfig()
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
static const ModuleDef* findModule(const GameConfig& cfg, const std::string& id)
|
||||
{
|
||||
for (const ModuleDef& m : cfg.modules.modules)
|
||||
{
|
||||
if (m.id == id) { return &m; }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static const ModuleStatModifier* findModifier(const ModuleDef& def, const std::string& stat)
|
||||
{
|
||||
for (const ModuleStatModifier& sm : def.statModifiers)
|
||||
{
|
||||
if (sm.stat == stat) { return &sm; }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
@@ -44,6 +62,72 @@ TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modul
|
||||
CHECK(sensor.statModifiers[0].formula.evaluate(1.0) == Approx(100.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ModuleDef* primer = findModule(cfg, "weapon_primer");
|
||||
REQUIRE(primer != nullptr);
|
||||
REQUIRE(primer->statModifiers.size() == 1);
|
||||
const ModuleStatModifier* sm = findModifier(*primer, "attack_rate");
|
||||
REQUIRE(sm != nullptr);
|
||||
CHECK(sm->modifierType == "multiplicative");
|
||||
CHECK(sm->formula.evaluate(1.0) == Approx(1.2));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ModuleDef* stab = findModule(cfg, "weapon_stabilizer");
|
||||
REQUIRE(stab != nullptr);
|
||||
REQUIRE(stab->statModifiers.size() == 2);
|
||||
|
||||
const ModuleStatModifier* rangeMod = findModifier(*stab, "attack_range");
|
||||
REQUIRE(rangeMod != nullptr);
|
||||
CHECK(rangeMod->modifierType == "multiplicative");
|
||||
CHECK(rangeMod->formula.evaluate(1.0) == Approx(1.5));
|
||||
|
||||
const ModuleStatModifier* rateMod = findModifier(*stab, "attack_rate");
|
||||
REQUIRE(rateMod != nullptr);
|
||||
CHECK(rateMod->modifierType == "multiplicative");
|
||||
CHECK(rateMod->formula.evaluate(1.0) == Approx(0.8));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ModuleDef* ab = findModule(cfg, "afterburner");
|
||||
REQUIRE(ab != nullptr);
|
||||
REQUIRE(ab->statModifiers.size() == 2);
|
||||
|
||||
const ModuleStatModifier* speedMod = findModifier(*ab, "speed");
|
||||
REQUIRE(speedMod != nullptr);
|
||||
CHECK(speedMod->modifierType == "multiplicative");
|
||||
CHECK(speedMod->formula.evaluate(1.0) == Approx(1.6));
|
||||
|
||||
const ModuleStatModifier* accelMod = findModifier(*ab, "main_acceleration");
|
||||
REQUIRE(accelMod != nullptr);
|
||||
CHECK(accelMod->modifierType == "additive");
|
||||
CHECK(accelMod->formula.evaluate(1.0) == Approx(60.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
const ModuleDef* mt = findModule(cfg, "maneuvering_thrusters");
|
||||
REQUIRE(mt != nullptr);
|
||||
REQUIRE(mt->statModifiers.size() == 2);
|
||||
|
||||
const ModuleStatModifier* speedMod = findModifier(*mt, "speed");
|
||||
REQUIRE(speedMod != nullptr);
|
||||
CHECK(speedMod->modifierType == "multiplicative");
|
||||
CHECK(speedMod->formula.evaluate(1.0) == Approx(1.2));
|
||||
|
||||
const ModuleStatModifier* accelMod = findModifier(*mt, "maneuvering_acceleration");
|
||||
REQUIRE(accelMod != nullptr);
|
||||
CHECK(accelMod->modifierType == "additive");
|
||||
CHECK(accelMod->formula.evaluate(1.0) == Approx(10.0));
|
||||
}
|
||||
|
||||
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
|
||||
@@ -4,17 +4,21 @@
|
||||
#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 "Tick.h"
|
||||
#include "WeaponComponent.h"
|
||||
|
||||
static GameConfig loadConfig()
|
||||
{
|
||||
@@ -33,6 +37,17 @@ static const ShipDef* findSchematic(const GameConfig& cfg, const std::string& id
|
||||
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)
|
||||
@@ -283,3 +298,219 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||
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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user