make recipe selection and buffers of smelter and reprocessing plant behave like other buildings, except that a recipe may be chosen automatically
This commit is contained in:
@@ -629,12 +629,13 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
||||
// Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1).
|
||||
// Output port (2,1) East. Input port example: (2,0) West.
|
||||
const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
|
||||
// Smelters have no recipe selection (REQ-BLD-SMELTER); they auto-accept any
|
||||
// ore/scrap that is an input to a smelter recipe.
|
||||
// A smelter starts with no recipe and picks one from the first material offered to
|
||||
// it (REQ-BLD-AUTO-RECIPE), which is what lets it accept the ore below.
|
||||
|
||||
// Complete construction (15s → tick 450+1 = 451 ticks).
|
||||
Tick tick = 0;
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||
REQUIRE(findBuilding(f.state, sid)->recipeId.empty());
|
||||
|
||||
// Place west-flowing belt at (2,0): belt flows West, delivers to smelter.
|
||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||
@@ -645,6 +646,13 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
||||
|
||||
const Building* b = findBuilding(f.state, sid);
|
||||
REQUIRE(b != nullptr);
|
||||
// The ore selected the recipe that consumes it, and its buffers were sized for that
|
||||
// recipe alone -- copper ore is not one of its inputs any more.
|
||||
REQUIRE(b->recipeId == "iron_ingot");
|
||||
REQUIRE(b->inputBuffer.caps.count(ItemType{"iron_ore"}) == 1);
|
||||
REQUIRE(b->inputBuffer.caps.count(ItemType{"copper_ore"}) == 0);
|
||||
REQUIRE(b->outputBuffer.caps.count(ItemType{"iron_ingot"}) == 1);
|
||||
REQUIRE(b->outputBuffer.caps.count(ItemType{"copper_ingot"}) == 0);
|
||||
// The item was accepted; it may still be travelling inward on the input belt,
|
||||
// so count buffered + in-transit (REQ-MAT-INPUT-INTAKE).
|
||||
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) >= 1);
|
||||
@@ -749,10 +757,10 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
|
||||
REQUIRE(hasIronIngot);
|
||||
}
|
||||
|
||||
// With mixed inputs, the smelter runs whichever recipe is currently satisfiable
|
||||
// and leaves an incomplete batch of another input waiting (see the union-of-
|
||||
// inputs caps in initAutoBuffers).
|
||||
TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete batch waits",
|
||||
// A belt carrying mixed ore is the realistic case for REQ-BLD-AUTO-RECIPE: the first ore
|
||||
// to arrive settles the recipe, and everything else on that belt is refused rather than
|
||||
// smelted alongside it.
|
||||
TEST_CASE("BuildingSystem: mixed ore on one belt leaves the smelter on the first ore's recipe",
|
||||
"[building]")
|
||||
{
|
||||
PlacementFixture f(kFastBeltSpeed_tps);
|
||||
@@ -762,8 +770,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete
|
||||
Tick tick = 0;
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||
|
||||
// Feed 1 iron_ore (iron_ingot needs 2 — incomplete) then 2 copper_ore
|
||||
// (copper_ingot needs 2 — satisfiable) via the west-flowing input belt.
|
||||
// Feed 1 iron_ore, then 2 copper_ore, via the west-flowing input belt.
|
||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||
const char* fed[] = { "iron_ore", "copper_ore", "copper_ore" };
|
||||
for (const char* id : fed)
|
||||
@@ -773,24 +780,26 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete
|
||||
f.bs.tickBeltPull(f.state);
|
||||
}
|
||||
|
||||
// copper_ingot cycle is 2.5s; run to completion.
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(secondsToTicks(2.5)) + 2, tick);
|
||||
|
||||
const Building* b = findBuilding(f.state, sid);
|
||||
REQUIRE(b != nullptr);
|
||||
|
||||
// Copper was smelted; the lone iron_ore still waits for a second unit.
|
||||
bool hasCopperIngot = false;
|
||||
// The iron ore came first, so the smelter smelts iron and nothing else. The copper
|
||||
// was never taken in, so no copper ingot was made.
|
||||
REQUIRE(b->recipeId == "iron_ingot");
|
||||
for (const Item& item : outputSideItems(*b))
|
||||
{
|
||||
if (item.type.id == "copper_ingot") { hasCopperIngot = true; }
|
||||
REQUIRE(item.type.id != "copper_ingot");
|
||||
}
|
||||
REQUIRE(hasCopperIngot);
|
||||
REQUIRE(b->pendingInputCount(ItemType{"copper_ore"}) == 0);
|
||||
|
||||
// The lone iron ore still waits for a second unit: the recipe needs two.
|
||||
const std::map<ItemType, int>::const_iterator ironIt =
|
||||
b->inputBuffer.counts.find(ItemType{"iron_ore"});
|
||||
REQUIRE(ironIt != b->inputBuffer.counts.end());
|
||||
REQUIRE(ironIt->second == 1);
|
||||
REQUIRE_FALSE(b->production.has_value());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -925,13 +934,16 @@ TEST_CASE("BuildingSystem: reprocessing plant sizes one output buffer per possib
|
||||
|
||||
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
||||
QPoint(0, 0), Rotation::East, 0).value();
|
||||
// Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the
|
||||
// single reprocessing recipe is applied automatically on completion.
|
||||
|
||||
// Complete construction (25s).
|
||||
Tick tick = 0;
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
|
||||
|
||||
// A plant holds no buffers until it has a recipe (REQ-BLD-AUTO-RECIPE); selecting
|
||||
// one sizes them, exactly as the first scrap offered to it would.
|
||||
REQUIRE(findBuilding(f.state, id)->outputBuffer.caps.empty());
|
||||
f.bs.setRecipe(f.state, id, "reprocessing_cycle");
|
||||
|
||||
const Building* b = findBuilding(f.state, id);
|
||||
REQUIRE(b != nullptr);
|
||||
// reprocessing_cycle outputs: 2 iron_ingot (60%), 1 circuit_board (30%),
|
||||
@@ -1029,30 +1041,133 @@ TEST_CASE("BuildingSystem: reprocessing plant runs a second cycle while holding
|
||||
REQUIRE(getProductionStatus(f.cfg, *b) == ProductionStatus::Producing);
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: one item's backlog does not block another item's cycle",
|
||||
// ---------------------------------------------------------------------------
|
||||
// Automatic recipe selection (REQ-BLD-AUTO-RECIPE)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Places a smelter and runs it to completion, leaving it with no recipe.
|
||||
static BuildingId buildSmelter(PlacementFixture& f, QPoint anchor, Tick& tick)
|
||||
{
|
||||
const BuildingId id =
|
||||
f.bs.place(f.state, BuildingType::Smelter, anchor, Rotation::East, 0).value();
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock,
|
||||
static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||
return id;
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: an unset auto-recipe building is unconfigured", "[building]")
|
||||
{
|
||||
// It holds no recipe until one is offered to it, so it reads grey like any other
|
||||
// unconfigured building (REQ-BLD-AUTO-RECIPE, REQ-UI-STATUS-LIGHT).
|
||||
PlacementFixture f;
|
||||
Tick tick = 0;
|
||||
const BuildingId id = buildSmelter(f, QPoint(0, 0), tick);
|
||||
|
||||
const Building* b = findBuilding(f.state, id);
|
||||
REQUIRE(b != nullptr);
|
||||
REQUIRE(b->recipeId.empty());
|
||||
REQUIRE(b->inputBuffer.caps.empty());
|
||||
REQUIRE(b->outputBuffer.caps.empty());
|
||||
REQUIRE(getProductionStatus(f.cfg, *b) == ProductionStatus::Unconfigured);
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: a set recipe is never replaced by a later material",
|
||||
"[building]")
|
||||
{
|
||||
// Per-item buffers, so a smelter holding iron ingots can still smelt copper
|
||||
// (REQ-MAT-OUTPUT-BUFFER). Under one shared capacity the iron would have blocked it.
|
||||
PlacementFixture f;
|
||||
// Once set the recipe is the player's to change: a material belonging to another of
|
||||
// its recipes is simply not an accepted input (REQ-BLD-AUTO-RECIPE).
|
||||
PlacementFixture f(kFastBeltSpeed_tps);
|
||||
Tick tick = 0;
|
||||
const BuildingId id = buildSmelter(f, QPoint(0, 0), tick);
|
||||
|
||||
Building smelter;
|
||||
smelter.type = BuildingType::Smelter;
|
||||
initAutoBuffers(f.cfg, smelter);
|
||||
// Iron ore first, which selects the iron recipe.
|
||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||
f.belts.tick();
|
||||
f.bs.tickBeltPull(f.state);
|
||||
REQUIRE(findBuilding(f.state, id)->recipeId == "iron_ingot");
|
||||
|
||||
// Copper ore in, and the iron_ingot buffer filled to its cap.
|
||||
smelter.inputBuffer.counts[ItemType{"copper_ore"}] =
|
||||
smelter.inputBuffer.caps.at(ItemType{"copper_ore"});
|
||||
const int ironCap = smelter.outputBuffer.caps.at(ItemType{"iron_ingot"});
|
||||
for (int i = 0; i < ironCap; ++i)
|
||||
{
|
||||
smelter.outputBuffer.items.push_back(makeItem("iron_ingot"));
|
||||
}
|
||||
// Copper ore next: refused, and the recipe stands.
|
||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("copper_ore"));
|
||||
f.belts.tick();
|
||||
f.bs.tickBeltPull(f.state);
|
||||
|
||||
REQUIRE_FALSE(outputBufferHasRoom(smelter, ItemType{"iron_ingot"}, 1));
|
||||
REQUIRE(outputBufferHasRoom(smelter, ItemType{"copper_ingot"}, 1));
|
||||
REQUIRE(canStartCycle(f.cfg, smelter));
|
||||
REQUIRE(getProductionStatus(f.cfg, smelter) == ProductionStatus::Producing);
|
||||
const Building* b = findBuilding(f.state, id);
|
||||
REQUIRE(b->recipeId == "iron_ingot");
|
||||
REQUIRE(b->pendingInputCount(ItemType{"copper_ore"}) == 0);
|
||||
// The copper is still sitting on the belt, refused rather than swallowed.
|
||||
REQUIRE(f.belts.peekItem(westPort(QPoint(2, 0))).has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: a manually selected recipe is not overridden", "[building]")
|
||||
{
|
||||
// The player's selection is a recipe like any other, so auto-selection stays out of
|
||||
// the way and the smelter refuses ore it does not smelt (REQ-BLD-AUTO-RECIPE).
|
||||
PlacementFixture f(kFastBeltSpeed_tps);
|
||||
Tick tick = 0;
|
||||
const BuildingId id = buildSmelter(f, QPoint(0, 0), tick);
|
||||
|
||||
f.bs.setRecipe(f.state, id, "copper_ingot");
|
||||
REQUIRE(findBuilding(f.state, id)->recipeId == "copper_ingot");
|
||||
|
||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||
f.belts.tick();
|
||||
f.bs.tickBeltPull(f.state);
|
||||
|
||||
const Building* b = findBuilding(f.state, id);
|
||||
REQUIRE(b->recipeId == "copper_ingot");
|
||||
REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: selecting a different recipe frees a stuck auto-recipe building",
|
||||
"[building]")
|
||||
{
|
||||
// A smelter left holding part of a cycle nothing feeds any more is freed by
|
||||
// selecting another recipe, which clears the buffers -- that is why no separate
|
||||
// clear action exists (REQ-BLD-AUTO-RECIPE, REQ-MAT-INPUT-BUFFER).
|
||||
PlacementFixture f(kFastBeltSpeed_tps);
|
||||
Tick tick = 0;
|
||||
const BuildingId id = buildSmelter(f, QPoint(0, 0), tick);
|
||||
|
||||
// One iron ore, where the recipe needs two: it can never run.
|
||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||
f.belts.tick();
|
||||
f.bs.tickBeltPull(f.state);
|
||||
runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 30, tick);
|
||||
|
||||
const Building* stuck = findBuilding(f.state, id);
|
||||
REQUIRE(stuck->recipeId == "iron_ingot");
|
||||
REQUIRE(stuck->pendingInputCount(ItemType{"iron_ore"}) == 1);
|
||||
REQUIRE_FALSE(stuck->production.has_value());
|
||||
|
||||
f.bs.setRecipe(f.state, id, "copper_ingot");
|
||||
const Building* freed = findBuilding(f.state, id);
|
||||
REQUIRE(freed->recipeId == "copper_ingot");
|
||||
REQUIRE(freed->pendingInputCount(ItemType{"iron_ore"}) == 0);
|
||||
REQUIRE(freed->inputBuffer.caps.count(ItemType{"copper_ore"}) == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: selecting (Auto) returns the building to automatic selection",
|
||||
"[building]")
|
||||
{
|
||||
// The dialog's clearing option unsets the recipe rather than leaving the building
|
||||
// idle for good: the next material offered selects one again (REQ-BLD-AUTO-RECIPE).
|
||||
PlacementFixture f(kFastBeltSpeed_tps);
|
||||
Tick tick = 0;
|
||||
const BuildingId id = buildSmelter(f, QPoint(0, 0), tick);
|
||||
|
||||
f.bs.setRecipe(f.state, id, "copper_ingot");
|
||||
f.bs.setRecipe(f.state, id, std::string()); // the "(Auto)" option
|
||||
REQUIRE(findBuilding(f.state, id)->recipeId.empty());
|
||||
|
||||
f.belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||
f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||
f.belts.tick();
|
||||
f.bs.tickBeltPull(f.state);
|
||||
|
||||
REQUIRE(findBuilding(f.state, id)->recipeId == "iron_ingot");
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then stalls",
|
||||
@@ -1063,8 +1178,8 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
||||
|
||||
const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant,
|
||||
QPoint(0, 0), Rotation::East, 0).value();
|
||||
// Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the
|
||||
// single reprocessing recipe is applied automatically on completion.
|
||||
// The plant selects its recipe from the first scrap offered to it
|
||||
// (REQ-BLD-AUTO-RECIPE), which is what the belt feeding below does.
|
||||
|
||||
// Complete construction (25s).
|
||||
Tick tick = 0;
|
||||
@@ -1242,10 +1357,12 @@ TEST_CASE("isConfigurableBuildingType: only types with player-facing settings",
|
||||
CHECK(isConfigurableBuildingType(BuildingType::Shipyard));
|
||||
CHECK(isConfigurableBuildingType(BuildingType::Splitter));
|
||||
|
||||
// Smelter and Reprocessing Plant run implicit recipes (REQ-BLD-SMELTER,
|
||||
// REQ-BLD-REPROCESSING) and the rest have no settings whatsoever.
|
||||
CHECK_FALSE(isConfigurableBuildingType(BuildingType::Smelter));
|
||||
CHECK_FALSE(isConfigurableBuildingType(BuildingType::ReprocessingPlant));
|
||||
// Smelter and Reprocessing Plant carry a recipe like any other, even though they can
|
||||
// also select it themselves (REQ-BLD-AUTO-RECIPE).
|
||||
CHECK(isConfigurableBuildingType(BuildingType::Smelter));
|
||||
CHECK(isConfigurableBuildingType(BuildingType::ReprocessingPlant));
|
||||
|
||||
// The rest have no settings whatsoever.
|
||||
CHECK_FALSE(isConfigurableBuildingType(BuildingType::SalvageBay));
|
||||
CHECK_FALSE(isConfigurableBuildingType(BuildingType::Belt));
|
||||
CHECK_FALSE(isConfigurableBuildingType(BuildingType::TunnelEntry));
|
||||
@@ -1295,8 +1412,15 @@ TEST_CASE("resolveBlueprintGhost: a partial overlap of the same type is invalid"
|
||||
// Both footprints stay on the asteroid, so terrain is not what fails here.
|
||||
f.bs.place(f.state, BuildingType::Smelter, QPoint(-3, 0), Rotation::East, 0);
|
||||
|
||||
CHECK(resolveOne(f, BuildingType::Smelter, QPoint(-2, 0), Rotation::East).action
|
||||
// Judged on where it sits, with no cursor to hit-test: the overlap is what decides.
|
||||
CHECK(resolveInConstellation(f, BuildingType::Smelter, QPoint(-2, 0), Rotation::East).action
|
||||
== BlueprintGhostAction::Invalid);
|
||||
|
||||
// With the cursor on the existing smelter the single-building gesture answers first
|
||||
// and hands it the settings, since a smelter carries a recipe (REQ-BLD-AUTO-RECIPE,
|
||||
// REQ-UI-BLUEPRINT-TRANSFER) -- the same as for a miner.
|
||||
CHECK(resolveOne(f, BuildingType::Smelter, QPoint(-2, 0), Rotation::East).action
|
||||
== BlueprintGhostAction::Transfer);
|
||||
}
|
||||
|
||||
TEST_CASE("resolveBlueprintGhost: a single configurable building transfers its settings",
|
||||
@@ -1457,11 +1581,11 @@ TEST_CASE("resolveBlueprintGhost: a constellation mixes transfers and plain over
|
||||
REQUIRE(miner.action == BlueprintGhostAction::Transfer);
|
||||
CHECK(*miner.targetId == minerId);
|
||||
|
||||
// A smelter runs an implicit recipe (REQ-BLD-SMELTER), so there is nothing to hand
|
||||
// over and it is simply left as it is.
|
||||
// A smelter carries a recipe too now (REQ-BLD-AUTO-RECIPE), so a blueprint of one
|
||||
// has something to hand over just as the miner does.
|
||||
const BlueprintGhostResolved smelter =
|
||||
resolveInConstellation(f, BuildingType::Smelter, QPoint(-5, 0), Rotation::East);
|
||||
REQUIRE(smelter.action == BlueprintGhostAction::CompatibleOverlap);
|
||||
REQUIRE(smelter.action == BlueprintGhostAction::Transfer);
|
||||
CHECK(*smelter.targetId == smelterId);
|
||||
}
|
||||
|
||||
@@ -1803,6 +1927,7 @@ TEST_CASE("BuildingSystem: getProductionStatus classifies production state", "[b
|
||||
REQUIRE(reprocessingRecipe->outputs.size() >= 2);
|
||||
|
||||
Building plant; plant.type = BuildingType::ReprocessingPlant;
|
||||
plant.recipeId = reprocessingRecipe->id;
|
||||
initBuffers(plant, *reprocessingRecipe);
|
||||
for (const RecipeIngredient& ing : reprocessingRecipe->inputs)
|
||||
{
|
||||
@@ -1829,11 +1954,33 @@ TEST_CASE("BuildingSystem: getProductionStatus classifies production state", "[b
|
||||
REQUIRE(statusOf(plant) == ProductionStatus::Starved);
|
||||
}
|
||||
|
||||
SECTION("Smelter (auto-recipe) is never grey")
|
||||
SECTION("Smelter: grey until it has a recipe, then judged like any other building")
|
||||
{
|
||||
// It holds no recipe until one is offered to it, so grey now applies to it too
|
||||
// (REQ-BLD-AUTO-RECIPE, REQ-UI-STATUS-LIGHT).
|
||||
Building smelter; smelter.type = BuildingType::Smelter;
|
||||
// No player-selectable recipe and empty inputs -> red, not grey.
|
||||
REQUIRE(statusOf(smelter) == ProductionStatus::Starved);
|
||||
REQUIRE(statusOf(smelter) == ProductionStatus::Unconfigured);
|
||||
|
||||
const RecipeDef* smelterRecipe = nullptr;
|
||||
for (const RecipeDef& r : f.cfg.recipes.recipes)
|
||||
{
|
||||
if (r.building == BuildingType::Smelter && !r.inputs.empty())
|
||||
{
|
||||
smelterRecipe = &r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
REQUIRE(smelterRecipe != nullptr);
|
||||
|
||||
smelter.recipeId = smelterRecipe->id;
|
||||
initBuffers(smelter, *smelterRecipe);
|
||||
REQUIRE(statusOf(smelter) == ProductionStatus::Starved); // recipe, but no ore
|
||||
|
||||
for (const RecipeIngredient& ing : smelterRecipe->inputs)
|
||||
{
|
||||
smelter.inputBuffer.counts[ItemType{ing.item}] = ing.amount;
|
||||
}
|
||||
REQUIRE(statusOf(smelter) == ProductionStatus::Producing);
|
||||
}
|
||||
|
||||
SECTION("Shipyard: unconfigured, then starved without materials, then producing")
|
||||
|
||||
Reference in New Issue
Block a user