Files
dota_factory/src/test/BuildingConfigTest.cpp

250 lines
8.9 KiB
C++

#include "catch.hpp"
#include "FactoryQueries.h"
#include <algorithm>
#include "Building.h"
#include "BuildingConfig.h"
#include "BuildingSystem.h"
#include "BuildingType.h"
#include "ConfigLoader.h"
#include "GameConfig.h"
#include "Rotation.h"
#include "ShipLayout.h"
#include "ShipsConfig.h"
#include "Simulation.h"
#include "SimulationTestAccess.h"
#include "TestConfig.h"
// readBuildingConfig underpins blueprint capture (REQ-UI-BLUEPRINT-STORAGE): it
// extracts a building's recipe / schematic / layout / splitter filters so they can be
// stored in the blueprint and reapplied on placement. It reads operational buildings
// and construction sites alike.
namespace
{
const BuildingDef* findDef(const GameConfig& cfg, BuildingType type)
{
for (const BuildingDef& def : cfg.buildings.buildings)
{
if (def.type == type) { return &def; }
}
return nullptr;
}
BuildingId placeOperational(Simulation& sim, const GameConfig& cfg,
BuildingType type, QPoint anchor)
{
const BuildingDef* def = findDef(cfg, type);
REQUIRE(def != nullptr);
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), type, def->surfaceMask, anchor, Rotation::East);
}
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
{
for (const ShipDef& def : cfg.ships.ships)
{
// A ship starts unlocked iff no unlock group grants it (REQ-LOCK-EXPLICIT).
bool granted = false;
for (const UnlockGroupDef& group : cfg.unlocks.groups)
{
if (std::find(group.ships.begin(), group.ships.end(), def.id) != group.ships.end())
{
granted = true;
break;
}
}
if (!granted) { return &def; }
}
return nullptr;
}
} // namespace
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
Simulation sim(loadTestConfig(), 7);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
CHECK(config->type == BuildingType::Miner);
REQUIRE(config->recipeId.has_value());
CHECK(*config->recipeId == "mine_iron_ore");
CHECK_FALSE(config->isSplitter);
CHECK_FALSE(config->shipLayout.has_value());
}
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
"[blueprint]")
{
const GameConfig cfg = loadTestConfig();
Simulation sim(loadTestConfig(), 7);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Assembler, QPoint(0, 0));
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
CHECK(config->type == BuildingType::Assembler);
CHECK_FALSE(config->recipeId.has_value()); // nothing to copy
CHECK_FALSE(config->isSplitter);
}
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
"[blueprint]")
{
const GameConfig cfg = loadTestConfig();
Simulation sim(loadTestConfig(), 7);
const ShipDef* schematic = findAvailableSchematic(cfg);
REQUIRE(schematic != nullptr);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, schematic->id);
// A real layout, not an empty one: applying an empty layout to a shipyard that has
// none is a no-op (REQ-MAT-INPUT-BUFFER), so it would leave nothing to read back.
ShipLayoutConfig layout;
PlacedModule placed;
placed.moduleId = "armor_plate";
placed.position = QPoint(0, 0);
placed.rotation = Rotation::East;
layout.placedModules.push_back(placed);
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
CHECK(config->type == BuildingType::Shipyard);
REQUIRE(config->recipeId.has_value());
CHECK(*config->recipeId == schematic->id);
REQUIRE(config->shipLayout.has_value());
REQUIRE(config->shipLayout->placedModules.size() == 1);
CHECK(config->shipLayout->placedModules[0].moduleId == "armor_plate");
}
TEST_CASE("readBuildingConfig reads a queued construction site", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
Simulation sim(loadTestConfig(), 7);
// A placed miner enters the construction queue as a site (not yet operational).
const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
REQUIRE(config.has_value());
CHECK(config->type == BuildingType::Miner);
REQUIRE(config->recipeId.has_value());
CHECK(*config->recipeId == "mine_iron_ore");
}
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[blueprint]")
{
Simulation sim(loadTestConfig(), 7);
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
}
// The derived facts a blueprint card shows (REQ-UI-BLUEPRINT-CARD). They live here
// rather than in the dialog so they can be tested: src/ui is off the test include path.
namespace
{
Blueprint makeBlueprint(const std::vector<BuildingType>& types)
{
Blueprint blueprint;
for (const BuildingType type : types)
{
BlueprintBuilding building;
building.type = type;
building.rotation = Rotation::East;
blueprint.buildings.push_back(building);
}
return blueprint;
}
} // namespace
TEST_CASE("computeBlueprintCost sums the constituent placement costs", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
// belt 2 + belt 2 + miner 15.
const Blueprint blueprint = makeBlueprint(
{BuildingType::Belt, BuildingType::Belt, BuildingType::Miner});
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 19);
}
TEST_CASE("computeBlueprintCost ignores types absent from buildings.toml", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
REQUIRE(cfg.buildings.findBuildingDef(BuildingType::Hq) == nullptr);
const Blueprint blueprint = makeBlueprint({BuildingType::Belt, BuildingType::Hq});
CHECK(computeBlueprintCost(blueprint, cfg.buildings) == 2);
}
TEST_CASE("computeBlueprintCost is zero for an empty blueprint", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
CHECK(computeBlueprintCost(Blueprint{}, cfg.buildings) == 0);
}
TEST_CASE("summarizeBlueprintContents orders by descending count", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
const Blueprint blueprint = makeBlueprint(
{BuildingType::Belt, BuildingType::Smelter, BuildingType::Miner,
BuildingType::Miner, BuildingType::Smelter, BuildingType::Miner});
const std::vector<BlueprintContentEntry> summary =
summarizeBlueprintContents(blueprint, cfg.buildings);
REQUIRE(summary.size() == 3);
CHECK(summary[0].buildingName == "Miner");
CHECK(summary[0].count == 3);
CHECK(summary[1].buildingName == "Smelter");
CHECK(summary[1].count == 2);
CHECK(summary[2].buildingName == "Belt");
CHECK(summary[2].count == 1);
}
TEST_CASE("summarizeBlueprintContents breaks ties in buildings.toml order", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
// Miner is declared after Belt in buildings.toml, which is the order the build
// button bar lays its buttons out, so Belt wins the tie despite the equal counts
// and despite sorting later by enum value.
const Blueprint blueprint = makeBlueprint(
{BuildingType::Miner, BuildingType::Belt, BuildingType::Miner, BuildingType::Belt});
const std::vector<BlueprintContentEntry> summary =
summarizeBlueprintContents(blueprint, cfg.buildings);
REQUIRE(summary.size() == 2);
CHECK(summary[0].buildingName == "Belt");
CHECK(summary[1].buildingName == "Miner");
}
TEST_CASE("summarizeBlueprintContents spells multi-word ids as display names", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
const Blueprint blueprint = makeBlueprint({BuildingType::TunnelEntry});
const std::vector<BlueprintContentEntry> summary =
summarizeBlueprintContents(blueprint, cfg.buildings);
REQUIRE(summary.size() == 1);
CHECK(summary[0].buildingName == "Tunnel Entry");
}
TEST_CASE("summarizeBlueprintContents is empty for an empty blueprint", "[blueprint]")
{
const GameConfig cfg = loadTestConfig();
CHECK(summarizeBlueprintContents(Blueprint{}, cfg.buildings).empty());
}