Files
dota_factory/src/test/ShipModuleTest.cpp

285 lines
9.0 KiB
C++

#include "catch.hpp"
#include "Building.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "ConfigLoader.h"
#include "EcsComponents.h"
#include "EntityAdmin.h"
#include "GameConfig.h"
#include "ItemType.h"
#include "ModulesConfig.h"
#include "Rotation.h"
#include "Ship.h"
#include "ShipLayout.h"
#include "ShipSystem.h"
#include "Simulation.h"
#include "Tick.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 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 sim.buildings().placeImmediate(
BuildingType::Shipyard,
yardDef.surfaceMask,
QPoint(0, 0),
Rotation::East);
}
static void fillMaterials(Simulation& sim, BuildingId yardId,
const ShipDef& def,
const ShipLayoutConfig& layout)
{
sim.buildings().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<Health>(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<Health>(e).maxHp == Approx(baseHp * 1.5f));
CHECK(sim.admin().get<Health>(e).hp == sim.admin().get<Health>(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 baseRange = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x));
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_formula = "10"
// final = base * 1.0 + 10 = base + 10
CHECK(sim.admin().get<SensorRange>(e).value == Approx(baseRange + 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<Health>(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);
sim.buildings().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);
sim.buildings().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);
sim.buildings().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);
sim.buildings().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);
sim.buildings().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);
sim.buildings().setShipLayout(yardId, layout);
const Building* b1 = sim.buildings().findBuilding(yardId);
REQUIRE(b1 != nullptr);
REQUIRE(b1->shipLayout.has_value());
sim.buildings().setRecipe(yardId, "destroyer");
const Building* b2 = sim.buildings().findBuilding(yardId);
REQUIRE(b2 != nullptr);
CHECK_FALSE(b2->shipLayout.has_value());
}