allow to set the recipe already for construction sites

This commit is contained in:
2026-06-22 21:32:24 +02:00
parent e5017ab3c5
commit d271d65678
10 changed files with 233 additions and 89 deletions

View File

@@ -54,6 +54,11 @@ struct ConstructionSite
std::string recipeId; // may be configured before completion
Tick completesAt = 0; // 0 = queued but not yet started
std::optional<ShipLayoutConfig> shipLayout;
// Splitter output filters configured before completion (REQ-BLD-SITE-CONFIG).
// Empty = accept all; applied to the BeltSystem when the splitter is built.
std::vector<ItemType> splitterFilterA;
std::vector<ItemType> splitterFilterB;
};
// A fully constructed, operational building.

View File

@@ -479,6 +479,44 @@ void BuildingSystem::setShipLayout(BuildingId id, const ShipLayoutConfig& layout
}
}
std::optional<BeltSystem::SplitterInfo>
BuildingSystem::getSiteSplitterInfo(BuildingId id) const
{
for (const ConstructionSite& site : m_constructionQueue)
{
if (site.id != id) { continue; }
if (site.type != BuildingType::Splitter) { return std::nullopt; }
const BuildingDef* def = findBuildingDef(site.type);
const ParsedSurfaceMask mask = parseSurfaceMask(
def ? def->surfaceMask : std::vector<std::string>{}, site.rotation);
if (mask.outputPorts.size() < 2) { return std::nullopt; }
BeltSystem::SplitterInfo info;
info.outputA = mask.outputPorts[0].direction;
info.outputB = mask.outputPorts[1].direction;
info.filterA = site.splitterFilterA;
info.filterB = site.splitterFilterB;
return info;
}
return std::nullopt;
}
void BuildingSystem::setSiteSplitterFilters(BuildingId id,
const std::vector<ItemType>& filterA,
const std::vector<ItemType>& filterB)
{
for (ConstructionSite& site : m_constructionQueue)
{
if (site.id == id && site.type == BuildingType::Splitter)
{
site.splitterFilterA = filterA;
site.splitterFilterB = filterB;
return;
}
}
}
// ---------------------------------------------------------------------------
// Tick hooks
// ---------------------------------------------------------------------------
@@ -564,6 +602,11 @@ void BuildingSystem::tickConstruction(Tick currentTick)
m_belts.placeSplitter(front.anchor,
mask.outputPorts[0].direction,
mask.outputPorts[1].direction);
// Carry over any filters configured while under construction
// (REQ-BLD-SITE-CONFIG).
m_belts.setSplitterFilters(front.anchor,
front.splitterFilterA,
front.splitterFilterB);
}
else if (front.type == BuildingType::TunnelEntry)
{

View File

@@ -70,6 +70,18 @@ public:
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
void setShipLayout(BuildingId id, const ShipLayoutConfig& layout);
// Splitter filter configuration for a queued/under-construction Splitter
// site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
// BeltSystem by tile; these mirror that for sites, which are not yet
// registered with BeltSystem. getSiteSplitterInfo returns the site's two
// output directions (derived from its surface mask) and stored filters, or
// nullopt if the id is not a Splitter site. The stored filters are applied
// to BeltSystem when the splitter finishes building (tickConstruction).
std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(BuildingId id) const;
void setSiteSplitterFilters(BuildingId id,
const std::vector<ItemType>& filterA,
const std::vector<ItemType>& filterB);
// -- Tick hooks (called from Simulation::tick in the documented order) ---
void tickConstruction(Tick currentTick);
void tickBeltPull();

View File

@@ -984,3 +984,42 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste
// Belt tile must still be registered after rotation — items can be placed on it.
REQUIRE(belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore")));
}
TEST_CASE("BuildingSystem: splitter filters configured on a construction site carry over "
"to the built splitter (REQ-BLD-SITE-CONFIG)", "[building]")
{
PlacementFixture f;
const QPoint tile(5, 5);
const BuildingId id = f.bs.place(BuildingType::Splitter, tile, Rotation::East, 0);
REQUIRE(id != kInvalidBuildingId);
REQUIRE(f.bs.findSite(id) != nullptr);
// Configure an output filter on the still-queued splitter site.
const std::vector<ItemType> filterA{ ItemType{"iron_ore"} };
const std::vector<ItemType> filterB{};
f.bs.setSiteSplitterFilters(id, filterA, filterB);
// The site reports its two output directions and the stored filters before
// it is built; it is not yet registered with BeltSystem.
const std::optional<BeltSystem::SplitterInfo> siteInfo = f.bs.getSiteSplitterInfo(id);
REQUIRE(siteInfo.has_value());
REQUIRE(siteInfo->filterA == filterA);
REQUIRE(siteInfo->filterB.empty());
REQUIRE_FALSE(f.belts.getSplitterInfo(tile).has_value());
// Run until construction completes.
Tick tick = 0;
while (f.bs.allBuildings().empty() && tick < 100000)
{
runTicks(f.bs, f.belts, 1, tick);
}
REQUIRE(f.bs.allBuildings().size() == 1);
REQUIRE(f.bs.allBuildings()[0].type == BuildingType::Splitter);
// The built splitter is registered with BeltSystem carrying the filters.
const std::optional<BeltSystem::SplitterInfo> builtInfo = f.belts.getSplitterInfo(tile);
REQUIRE(builtInfo.has_value());
REQUIRE(builtInfo->filterA == filterA);
REQUIRE(builtInfo->filterB.empty());
}

View File

@@ -193,18 +193,26 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
const double prevSpeed = m_gameWorldView->gameSpeed();
m_gameWorldView->setGameSpeed(0.0);
// A construction site has no Building yet; fall back to its site record so
// the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG).
const Building* b = m_sim->buildings().findBuilding(event->shipyardId);
if (!b)
const ConstructionSite* s =
b ? nullptr : m_sim->buildings().findSite(event->shipyardId);
if (!b && !s)
{
m_gameWorldView->setGameSpeed(prevSpeed);
m_gameWorldView->resetFrameTimer();
return;
}
const std::string& schematicId = b ? b->recipeId : s->recipeId;
const std::optional<ShipLayoutConfig>& layoutOpt =
b ? b->shipLayout : s->shipLayout;
ShipLayoutConfig currentLayout;
if (b->shipLayout.has_value())
if (layoutOpt.has_value())
{
currentLayout = *b->shipLayout;
currentLayout = *layoutOpt;
}
std::set<std::string> unlockedModuleIds;
@@ -218,7 +226,7 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
moduleLevels[def.id] = m_sim->moduleSchematicLevel(def.id);
}
ShipLayoutDialog dialog(&m_sim->config(), b->recipeId, currentLayout,
ShipLayoutDialog dialog(&m_sim->config(), schematicId, currentLayout,
m_layoutBlueprints,
std::move(unlockedModuleIds),
std::move(moduleLevels),
@@ -238,17 +246,22 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
const double prevSpeed = m_gameWorldView->gameSpeed();
m_gameWorldView->setGameSpeed(0.0);
// A construction site has no Building yet; fall back to its site record so
// the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG).
const Building* b = m_sim->buildings().findBuilding(event->buildingId);
if (!b)
const ConstructionSite* s =
b ? nullptr : m_sim->buildings().findSite(event->buildingId);
if (!b && !s)
{
m_gameWorldView->setGameSpeed(prevSpeed);
m_gameWorldView->resetFrameTimer();
return;
}
const BuildingType type = b ? b->type : s->type;
const std::vector<RecipeSelectionOption> options =
buildRecipeSelectionOptions(*b, *m_sim, m_sim->config());
const QString title = (b->type == BuildingType::Shipyard)
buildRecipeSelectionOptions(type, *m_sim, m_sim->config());
const QString title = (type == BuildingType::Shipyard)
? tr("Select Schematic")
: tr("Select Recipe");

View File

@@ -88,12 +88,12 @@ QString shipTooltip(const ShipDef& def)
} // namespace
std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
const Building& building, Simulation& sim, const GameConfig& config)
BuildingType type, Simulation& sim, const GameConfig& config)
{
std::vector<RecipeSelectionOption> options;
options.push_back({std::string(), QObject::tr("(None)"), QString()});
if (building.type == BuildingType::Shipyard)
if (type == BuildingType::Shipyard)
{
for (const ShipDef& def : config.ships.ships)
{
@@ -107,9 +107,9 @@ std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
{
for (const RecipeDef& recipe : config.recipes.recipes)
{
if (recipe.building != building.type) { continue; }
if ((building.type == BuildingType::Miner
|| building.type == BuildingType::Assembler)
if (recipe.building != type) { continue; }
if ((type == BuildingType::Miner
|| type == BuildingType::Assembler)
&& !sim.isRecipeUnlocked(recipe.id))
{
continue;

View File

@@ -7,7 +7,8 @@
#include <QDialog>
#include <QString>
struct Building;
#include "BuildingType.h"
struct GameConfig;
class Simulation;
class QPushButton;
@@ -21,13 +22,14 @@ struct RecipeSelectionOption
QString tooltip;
};
// Builds the lock-aware list of selectable options for a production building,
// with display captions and info tooltips (REQ-UI-SELECT-BUTTON,
// REQ-UI-SELECT-TOOLTIP). The first entry is always a "(None)" option with an
// empty id. Shared by the selection dialog and the panel selection button so
// both render identical captions and tooltips.
// Builds the lock-aware list of selectable options for a production building
// of the given type, with display captions and info tooltips
// (REQ-UI-SELECT-BUTTON, REQ-UI-SELECT-TOOLTIP). The first entry is always a
// "(None)" option with an empty id. Shared by the selection dialog and the
// panel selection button so both render identical captions and tooltips, and
// usable for construction sites which have no Building yet (REQ-BLD-SITE-CONFIG).
std::vector<RecipeSelectionOption> buildRecipeSelectionOptions(
const Building& building, Simulation& sim, const GameConfig& config);
BuildingType type, Simulation& sim, const GameConfig& config);
// Modal dialog showing a grid of option buttons (REQ-UI-SELECT-BUTTON). The
// game is paused by the caller while it is open. Clicking an option selects it

View File

@@ -237,62 +237,40 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
m_singleBuildingId = id;
hideAllWidgets();
const Building* b = m_sim->buildings().findBuilding(id);
if (!b)
const Building* b = m_sim->buildings().findBuilding(id);
const ConstructionSite* s = b ? nullptr : m_sim->buildings().findSite(id);
if (!b && !s)
{
const ConstructionSite* s = m_sim->buildings().findSite(id);
if (!s)
{
buildEmpty();
return;
}
QString progress;
if (s->completesAt == 0)
{
progress = tr("Queued");
}
else
{
const BuildingDef* def = nullptr;
for (const BuildingDef& d : m_config->buildings.buildings)
{
if (d.type == s->type) { def = &d; break; }
}
if (def && def->constructionTimeSeconds > 0)
{
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
const Tick elapsed = m_sim->currentTick() - (s->completesAt - duration);
const int pct = static_cast<int>(
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
progress = tr("%1% complete").arg(pct);
}
else
{
progress = tr("Building...");
}
}
m_titleLabel->setText(tr("(Building) %1").arg(buildingTypeName(s->type)));
m_titleLabel->show();
m_buffersLabel->setText(progress);
m_buffersLabel->show();
buildEmpty();
return;
}
m_singleIsSite = (s != nullptr);
m_titleLabel->setText(buildingTypeName(b->type));
// A construction site exposes the same configuration as the operational
// building it will become (REQ-BLD-SITE-CONFIG). The only difference is
// that its buffer/production rows are replaced by a construction-progress
// line, since a site has no buffers and runs no production cycle.
const BuildingType type = b ? b->type : s->type;
const std::string& recipeId = b ? b->recipeId : s->recipeId;
const std::optional<ShipLayoutConfig>& shipLayout =
b ? b->shipLayout : s->shipLayout;
const QPoint anchor = b ? b->anchor : s->anchor;
m_titleLabel->setText(m_singleIsSite
? tr("(Building) %1").arg(buildingTypeName(type))
: buildingTypeName(type));
m_titleLabel->show();
m_buffersLabel->show();
if (isProductionBuilding(b->type))
if (isProductionBuilding(type))
{
const std::vector<RecipeSelectionOption> options =
buildRecipeSelectionOptions(*b, *m_sim, *m_config);
buildRecipeSelectionOptions(type, *m_sim, *m_config);
const RecipeSelectionOption* current = nullptr;
for (const RecipeSelectionOption& option : options)
{
if (option.id == b->recipeId)
if (option.id == recipeId)
{
current = &option;
break;
@@ -306,7 +284,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
}
else
{
const QString placeholder = (b->type == BuildingType::Shipyard)
const QString placeholder = (type == BuildingType::Shipyard)
? tr("Select schematic")
: tr("Select recipe");
m_recipeSelectButton->setText(placeholder);
@@ -314,15 +292,15 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
}
m_recipeSelectButton->show();
if (b->type == BuildingType::Shipyard && !b->recipeId.empty())
if (type == BuildingType::Shipyard && !recipeId.empty())
{
const ShipDef* sDef = findShipDef(b->recipeId);
const ShipDef* sDef = findShipDef(recipeId);
if (sDef && !sDef->layout.empty())
{
ShipLayoutConfig layout;
if (b->shipLayout.has_value())
if (shipLayout.has_value())
{
layout = *b->shipLayout;
layout = *shipLayout;
}
m_layoutPreview->setShipAndLayout(
sDef->layout, layout, &m_config->modules.modules);
@@ -348,7 +326,9 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
m_configureLayoutBtn->hide();
}
if (isBeltLike(b->type))
// Belt "Clear" removes items from a live belt tile; a construction site has
// none and is not registered with BeltSystem yet, so hide it for sites.
if (isBeltLike(type) && !m_singleIsSite)
{
m_clearBeltBtn->show();
}
@@ -357,10 +337,19 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
m_clearBeltBtn->hide();
}
if (b->type == BuildingType::Splitter)
if (type == BuildingType::Splitter)
{
m_splitterTile = b->anchor;
buildSplitterFilters(m_splitterTile);
std::optional<BeltSystem::SplitterInfo> info;
if (m_singleIsSite)
{
info = m_sim->buildings().getSiteSplitterInfo(id);
}
else
{
m_splitterTile = anchor;
info = m_sim->belts().getSplitterInfo(m_splitterTile);
}
buildSplitterFilters(info);
}
else
{
@@ -370,7 +359,39 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
m_filterBList->hide();
}
refreshBuffers(b);
if (m_singleIsSite)
{
QString progress;
if (s->completesAt == 0)
{
progress = tr("Queued");
}
else
{
const BuildingDef* def = nullptr;
for (const BuildingDef& d : m_config->buildings.buildings)
{
if (d.type == s->type) { def = &d; break; }
}
if (def && def->constructionTimeSeconds > 0)
{
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
const Tick elapsed = m_sim->currentTick() - (s->completesAt - duration);
const int pct = static_cast<int>(
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
progress = tr("%1% complete").arg(pct);
}
else
{
progress = tr("Building...");
}
}
m_buffersLabel->setText(progress);
}
else
{
refreshBuffers(b);
}
}
void SelectedBuildingPanel::refreshBuffers(const Building* b)
@@ -632,10 +653,9 @@ void SelectedBuildingPanel::onSelectRecipeClicked()
rebuild();
}
void SelectedBuildingPanel::buildSplitterFilters(QPoint splitterTile)
void SelectedBuildingPanel::buildSplitterFilters(
const std::optional<BeltSystem::SplitterInfo>& info)
{
const std::optional<BeltSystem::SplitterInfo> info =
m_sim->belts().getSplitterInfo(splitterTile);
if (!info.has_value())
{
m_filterALabel->hide();
@@ -698,10 +718,20 @@ void SelectedBuildingPanel::onSplitterFilterChanged()
return filter;
};
m_sim->belts().setSplitterFilters(
m_splitterTile,
collectFilter(m_filterAList),
collectFilter(m_filterBList));
if (m_singleIsSite)
{
m_sim->buildings().setSiteSplitterFilters(
m_singleBuildingId,
collectFilter(m_filterAList),
collectFilter(m_filterBList));
}
else
{
m_sim->belts().setSplitterFilters(
m_splitterTile,
collectFilter(m_filterAList),
collectFilter(m_filterBList));
}
}
std::vector<std::string> SelectedBuildingPanel::allItemIds() const

View File

@@ -9,6 +9,7 @@
#include "entt/entity/entity.hpp"
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingId.h"
#include "DebugDrawToggledEvent.h"
@@ -63,7 +64,7 @@ private:
void buildSingle(BuildingId id);
void buildMulti(const std::vector<BuildingId>& ids);
void refreshBuffers(const Building* b);
void buildSplitterFilters(QPoint splitterTile);
void buildSplitterFilters(const std::optional<BeltSystem::SplitterInfo>& info);
const RecipeDef* findRecipe(const Building* b) const;
const ShipDef* findShipDef(const std::string& id) const;
std::vector<std::string> allItemIds() const;
@@ -86,6 +87,7 @@ private:
QPushButton* m_configureLayoutBtn;
BuildingId m_singleBuildingId;
bool m_singleIsSite = false; // selected single entity is a construction site
QPoint m_splitterTile;
std::string m_currentRecipeId;