REQ-BLD-COPY-CONFIG is dropped: Shift+right-click no longer caches a building's recipe / schematic+layout / splitter filters, Shift+left-click no longer stamps them onto same-type buildings, and the cyan eligible- target tint and copy/paste flashes go with them. Shift+left-click now falls through to normal selection, which needs no code of its own -- only Ctrl is meaningful to selection (REQ-UI-MULTI-SELECT), so dropping the branch that consumed the click is enough. readBuildingConfig stays. It had two callers, and the other one is captureBlueprintFromSelection, which needs it to record each blueprint building's configuration (REQ-UI-BLUEPRINT-STORAGE). Its five tests stay too, retagged [blueprint] and rewritten to name the caller that is left. The visuals.toml copy_config colour is removed together with its VisualsConfig field and its loader line: overlay keys are mandatory, so those three have to move as one or startup aborts. REQ-BLD-COPY-CONFIG-FEEDBACK, cited by four of these files and by the TOML comment, never existed in requirements.md; deleting the citations retires a dangling id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
239 lines
8.5 KiB
C++
239 lines
8.5 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);
|
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, ShipLayoutConfig{});
|
|
|
|
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);
|
|
CHECK(config->shipLayout.has_value());
|
|
}
|
|
|
|
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());
|
|
}
|