fix bug where splitter filters were not taken over to blueprint

This commit is contained in:
2026-06-23 21:08:46 +02:00
parent d271d65678
commit 577927ef70
6 changed files with 123 additions and 2 deletions

View File

@@ -455,13 +455,13 @@ The screen is divided into two columns: a main column (75% width) containing the
- REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list. - REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list.
- REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards) at the time of capture. If no recipe or schematic was selected at capture time, none is stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building). - REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards), and for splitters the two output filters (each a list of item types; an empty list means accept-all), at the time of capture. If no recipe or schematic was selected at capture time, none is stored; for a splitter with no filters set, no filter lists are stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building, with the splitter filters as `filter_a`/`filter_b` arrays of item-type ids).
- REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint. - REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint.
- REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). Pressing Q/E rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint. - REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). Pressing Q/E rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint.
- REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately. - REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. If splitter output filters are stored, they are applied to the construction site immediately and carry over when it finishes building (REQ-BLD-SITE-CONFIG). Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited. - REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited.

View File

@@ -51,6 +51,24 @@ std::string serialize(const std::vector<Blueprint>& blueprints)
bldTbl.insert("offset_x", static_cast<int64_t>(b.offset.x())); bldTbl.insert("offset_x", static_cast<int64_t>(b.offset.x()));
bldTbl.insert("offset_y", static_cast<int64_t>(b.offset.y())); bldTbl.insert("offset_y", static_cast<int64_t>(b.offset.y()));
bldTbl.insert("recipe_id", b.recipeId); bldTbl.insert("recipe_id", b.recipeId);
if (!b.splitterFilterA.empty())
{
toml::array filterArr;
for (const ItemType& item : b.splitterFilterA)
{
filterArr.push_back(item.id);
}
bldTbl.insert("filter_a", std::move(filterArr));
}
if (!b.splitterFilterB.empty())
{
toml::array filterArr;
for (const ItemType& item : b.splitterFilterB)
{
filterArr.push_back(item.id);
}
bldTbl.insert("filter_b", std::move(filterArr));
}
if (b.shipLayout.has_value()) if (b.shipLayout.has_value())
{ {
toml::array modArr; toml::array modArr;
@@ -138,6 +156,28 @@ std::vector<Blueprint> deserialize(const std::string& tomlContent)
bb.offset.setX(static_cast<int>((*bldTbl)["offset_x"].value_or(int64_t{0}))); bb.offset.setX(static_cast<int>((*bldTbl)["offset_x"].value_or(int64_t{0})));
bb.offset.setY(static_cast<int>((*bldTbl)["offset_y"].value_or(int64_t{0}))); bb.offset.setY(static_cast<int>((*bldTbl)["offset_y"].value_or(int64_t{0})));
bb.recipeId = (*bldTbl)["recipe_id"].value_or(std::string{}); bb.recipeId = (*bldTbl)["recipe_id"].value_or(std::string{});
const toml::array* filterAArr = (*bldTbl)["filter_a"].as_array();
if (filterAArr)
{
for (std::size_t k = 0; k < filterAArr->size(); ++k)
{
const std::optional<std::string> itemId =
(*filterAArr)[k].value<std::string>();
if (itemId) { bb.splitterFilterA.push_back(ItemType{*itemId}); }
}
}
const toml::array* filterBArr = (*bldTbl)["filter_b"].as_array();
if (filterBArr)
{
for (std::size_t k = 0; k < filterBArr->size(); ++k)
{
const std::optional<std::string> itemId =
(*filterBArr)[k].value<std::string>();
if (itemId) { bb.splitterFilterB.push_back(ItemType{*itemId}); }
}
}
const toml::array* modArr = (*bldTbl)["modules"].as_array(); const toml::array* modArr = (*bldTbl)["modules"].as_array();
if (modArr) if (modArr)
{ {

View File

@@ -8,6 +8,7 @@
#include <QString> #include <QString>
#include "BuildingType.h" #include "BuildingType.h"
#include "ItemType.h"
#include "Rotation.h" #include "Rotation.h"
#include "ShipLayout.h" #include "ShipLayout.h"
@@ -18,6 +19,11 @@ struct BlueprintBuilding
QPoint offset; // tile offset from bounding-box center (floor for even sizes) QPoint offset; // tile offset from bounding-box center (floor for even sizes)
std::string recipeId; // empty = none selected std::string recipeId; // empty = none selected
std::optional<ShipLayoutConfig> shipLayout; std::optional<ShipLayoutConfig> shipLayout;
// Splitter output filters captured at blueprint creation (REQ-UI-BLUEPRINT-STORAGE).
// Empty = accept all. Re-applied to the placed splitter site (REQ-UI-BLUEPRINT-PLACE).
std::vector<ItemType> splitterFilterA;
std::vector<ItemType> splitterFilterB;
}; };
struct Blueprint struct Blueprint

View File

@@ -10,6 +10,7 @@
#include "Blueprint.h" #include "Blueprint.h"
#include "BlueprintSerializer.h" #include "BlueprintSerializer.h"
#include "BuildingType.h" #include "BuildingType.h"
#include "ItemType.h"
#include "Rotation.h" #include "Rotation.h"
#include "ShipLayout.h" #include "ShipLayout.h"
@@ -263,3 +264,42 @@ TEST_CASE("BlueprintSerializer: shipyard with empty modules list round-trips", "
REQUIRE(loaded[0].buildings[0].shipLayout.has_value()); REQUIRE(loaded[0].buildings[0].shipLayout.has_value());
REQUIRE(loaded[0].buildings[0].shipLayout->placedModules.empty()); REQUIRE(loaded[0].buildings[0].shipLayout->placedModules.empty());
} }
// ---------------------------------------------------------------------------
// Splitter output filters round-trip through TOML serialization
// (REQ-UI-BLUEPRINT-STORAGE).
// ---------------------------------------------------------------------------
TEST_CASE("BlueprintSerializer: splitter output filters round-trip", "[serializer]")
{
BlueprintBuilding splitter =
makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0));
splitter.splitterFilterA = { ItemType{"iron_ore"}, ItemType{"copper_ore"} };
splitter.splitterFilterB = { ItemType{"scrap"} };
const Blueprint original = makeBlueprintWith({ splitter }, "Sorter");
const std::string toml = BlueprintSerializer::serialize({ original });
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
REQUIRE(loaded.size() == 1);
REQUIRE(loaded[0].buildings.size() == 1);
const BlueprintBuilding& b = loaded[0].buildings[0];
REQUIRE(b.splitterFilterA
== std::vector<ItemType>{ ItemType{"iron_ore"}, ItemType{"copper_ore"} });
REQUIRE(b.splitterFilterB == std::vector<ItemType>{ ItemType{"scrap"} });
}
TEST_CASE("BlueprintSerializer: splitter with no filters round-trips empty", "[serializer]")
{
const Blueprint original = makeBlueprintWith(
{ makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0)) }, "Open");
const std::string toml = BlueprintSerializer::serialize({ original });
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
REQUIRE(loaded.size() == 1);
REQUIRE(loaded[0].buildings.size() == 1);
REQUIRE(loaded[0].buildings[0].splitterFilterA.empty());
REQUIRE(loaded[0].buildings[0].splitterFilterB.empty());
}

View File

@@ -201,6 +201,16 @@ Blueprint BlueprintPanel::createBlueprintFromSelection() const
bb.offset = e.building->anchor - center; bb.offset = e.building->anchor - center;
bb.recipeId = e.building->recipeId; bb.recipeId = e.building->recipeId;
bb.shipLayout = e.building->shipLayout; bb.shipLayout = e.building->shipLayout;
if (e.building->type == BuildingType::Splitter)
{
const std::optional<BeltSystem::SplitterInfo> info =
m_sim->belts().getSplitterInfo(e.building->anchor);
if (info.has_value())
{
bb.splitterFilterA = info->filterA;
bb.splitterFilterB = info->filterB;
}
}
bp.buildings.push_back(bb); bp.buildings.push_back(bb);
} }
return bp; return bp;

View File

@@ -58,6 +58,19 @@
namespace namespace
{ {
// Keep only the filter entries whose item type is currently unlocked
// (REQ-LOCK-UI-BLUEPRINT). An empty result means "accept all".
std::vector<ItemType> filterUnlockedItems(const std::vector<ItemType>& filter,
const Simulation& sim)
{
std::vector<ItemType> result;
for (const ItemType& item : filter)
{
if (sim.isItemUnlocked(item.id)) { result.push_back(item); }
}
return result;
}
Rotation rotateClockwise(Rotation r) Rotation rotateClockwise(Rotation r)
{ {
switch (r) switch (r)
@@ -532,6 +545,18 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
{ {
m_sim->buildings().setShipLayout(id, *bb.shipLayout); m_sim->buildings().setShipLayout(id, *bb.shipLayout);
} }
if (bb.type == BuildingType::Splitter
&& (!bb.splitterFilterA.empty() || !bb.splitterFilterB.empty()))
{
// The splitter is still a construction site, so the filters carry
// over when it finishes building (REQ-UI-BLUEPRINT-PLACE). Locked
// item types are dropped per REQ-LOCK-UI-BLUEPRINT.
m_sim->buildings().setSiteSplitterFilters(
id,
filterUnlockedItems(bb.splitterFilterA, *m_sim),
filterUnlockedItems(bb.splitterFilterB, *m_sim));
}
} }
} }