Auto-process smelter and reprocessing plant (no recipe selection)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "BuildingSystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
@@ -9,6 +10,18 @@
|
||||
#include "SurfaceMask.h"
|
||||
#include "tracing.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Smelter and Reprocessing Plant have no player-selected recipe
|
||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). They auto-process whatever inputs
|
||||
// they receive, matching against every recipe of their building type.
|
||||
bool isAutoRecipeBuildingType(BuildingType type)
|
||||
{
|
||||
return type == BuildingType::Smelter
|
||||
|| type == BuildingType::ReprocessingPlant;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
BuildingSystem::BuildingSystem(const GameConfig& config,
|
||||
BeltSystem& belts,
|
||||
std::function<BuildingId()> allocateBuildingId,
|
||||
@@ -118,6 +131,56 @@ void BuildingSystem::initBuffers(Building& b, const RecipeDef& recipe) const
|
||||
}
|
||||
}
|
||||
|
||||
void BuildingSystem::initAutoBuffers(Building& b) const
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
b.inputBuffer.caps.clear();
|
||||
|
||||
// Union the inputs of every recipe of this building type; the cap for each
|
||||
// item is twice the largest per-cycle requirement across those recipes.
|
||||
// Output capacity follows the same rules as initBuffers: the Reprocessing
|
||||
// Plant holds one cycle's max output (REQ-MAT-OUTPUT-BUFFER-REPROCESSING),
|
||||
// other auto buildings hold twice the largest per-cycle output.
|
||||
int outputCapacity = 0;
|
||||
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
||||
{
|
||||
if (recipe.building != b.type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const RecipeIngredient& ing : recipe.inputs)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
b.inputBuffer.counts[type] = 0;
|
||||
b.inputBuffer.caps[type] =
|
||||
std::max(b.inputBuffer.caps[type], 2 * ing.amount);
|
||||
}
|
||||
|
||||
if (b.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
int maxAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
maxAmount = std::max(maxAmount, out.amount);
|
||||
}
|
||||
outputCapacity = std::max(outputCapacity, maxAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
int totalAmount = 0;
|
||||
for (const RecipeOutput& out : recipe.outputs)
|
||||
{
|
||||
totalAmount += out.amount;
|
||||
}
|
||||
outputCapacity = std::max(outputCapacity, 2 * totalAmount);
|
||||
}
|
||||
}
|
||||
|
||||
b.outputBuffer.items.clear();
|
||||
b.outputBuffer.capacity = outputCapacity;
|
||||
}
|
||||
|
||||
void BuildingSystem::initShipyardBuffers(Building& b) const
|
||||
{
|
||||
b.inputBuffer.counts.clear();
|
||||
@@ -419,6 +482,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
{
|
||||
if (site.id == id)
|
||||
{
|
||||
// Auto-recipe buildings have no player-selected recipe
|
||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
|
||||
if (isAutoRecipeBuildingType(site.type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// No-op if the recipe is unchanged, so a redundant selection does
|
||||
// not wipe an already-configured ship layout.
|
||||
if (site.recipeId == recipeId)
|
||||
@@ -436,6 +505,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
{
|
||||
if (building.id == id)
|
||||
{
|
||||
// Auto-recipe buildings have no player-selected recipe
|
||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING); ignore any attempt to set one.
|
||||
if (isAutoRecipeBuildingType(building.type))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// No-op if the recipe is unchanged, so a redundant selection does
|
||||
// not wipe an already-configured ship layout or reset buffers.
|
||||
if (building.recipeId == recipeId)
|
||||
@@ -603,6 +678,12 @@ void BuildingSystem::tickConstruction(Tick currentTick)
|
||||
{
|
||||
initSalvageBayBuffer(building);
|
||||
}
|
||||
else if (isAutoRecipeBuildingType(building.type))
|
||||
{
|
||||
// Smelter/Reprocessing Plant need no recipe selection; buffers are set
|
||||
// up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||
initAutoBuffers(building);
|
||||
}
|
||||
else if (!building.recipeId.empty())
|
||||
{
|
||||
if (building.type == BuildingType::Shipyard)
|
||||
@@ -684,18 +765,24 @@ void BuildingSystem::tickBeltPull()
|
||||
continue;
|
||||
}
|
||||
|
||||
if (building.recipeId.empty())
|
||||
// Auto-recipe buildings (Smelter, Reprocessing Plant) accept any item
|
||||
// that is an input to one of their recipes; their caps already span the
|
||||
// union of those inputs (initAutoBuffers), so no recipe lookup is needed.
|
||||
if (!isAutoRecipeBuildingType(building.type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (building.type != BuildingType::Shipyard)
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (!recipe || recipe->inputs.empty())
|
||||
if (building.recipeId.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (building.type != BuildingType::Shipyard)
|
||||
{
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (!recipe || recipe->inputs.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const Port& port : building.inputPorts)
|
||||
@@ -752,18 +839,15 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (building.recipeId.empty())
|
||||
const bool autoRecipe = isAutoRecipeBuildingType(building.type);
|
||||
if (!autoRecipe && building.recipeId.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (!recipe)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If a production cycle is active, check for completion.
|
||||
// If a production cycle is active, check for completion. Completion only
|
||||
// needs the already-decided outputs, so it does not depend on which
|
||||
// recipe is selected or auto-chosen.
|
||||
if (building.production)
|
||||
{
|
||||
if (currentTick >= building.production->completesAt)
|
||||
@@ -779,66 +863,93 @@ void BuildingSystem::tickProduction(Tick currentTick)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Idle: check if a new cycle can start.
|
||||
|
||||
// 1. All required inputs present?
|
||||
bool inputsOk = true;
|
||||
for (const RecipeIngredient& ing : recipe->inputs)
|
||||
// Idle: gather the candidate recipes to try. Auto-recipe buildings
|
||||
// (Smelter, Reprocessing Plant) have no selected recipe and try every
|
||||
// recipe of their type in config order, running the first whose inputs
|
||||
// are satisfied (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). Other buildings
|
||||
// try only their selected recipe.
|
||||
std::vector<const RecipeDef*> candidates;
|
||||
if (autoRecipe)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
const std::map<ItemType, int>::const_iterator it =
|
||||
building.inputBuffer.counts.find(type);
|
||||
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
|
||||
if (have < ing.amount)
|
||||
for (const RecipeDef& r : m_config.recipes.recipes)
|
||||
{
|
||||
inputsOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inputsOk)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Determine chosen outputs (roll for reprocessing).
|
||||
std::vector<Item> chosen;
|
||||
if (building.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
chosen = rollReprocessingOutput(*recipe);
|
||||
if (chosen.empty()) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const RecipeOutput& out : recipe->outputs)
|
||||
{
|
||||
Item item;
|
||||
item.type.id = out.item;
|
||||
for (int i = 0; i < out.amount; ++i)
|
||||
if (r.building == building.type && !r.inputs.empty())
|
||||
{
|
||||
chosen.push_back(item);
|
||||
candidates.push_back(&r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Output buffer has space for chosen outputs?
|
||||
const int newSize = static_cast<int>(building.outputBuffer.items.size())
|
||||
+ static_cast<int>(chosen.size());
|
||||
if (newSize > building.outputBuffer.capacity)
|
||||
else
|
||||
{
|
||||
continue;
|
||||
const RecipeDef* recipe = findRecipe(building.recipeId, building.type);
|
||||
if (recipe)
|
||||
{
|
||||
candidates.push_back(recipe);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Consume inputs and start cycle.
|
||||
for (const RecipeIngredient& ing : recipe->inputs)
|
||||
for (const RecipeDef* recipe : candidates)
|
||||
{
|
||||
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
|
||||
}
|
||||
// 1. All required inputs present?
|
||||
bool inputsOk = true;
|
||||
for (const RecipeIngredient& ing : recipe->inputs)
|
||||
{
|
||||
const ItemType type{ing.item};
|
||||
const std::map<ItemType, int>::const_iterator it =
|
||||
building.inputBuffer.counts.find(type);
|
||||
const int have = (it != building.inputBuffer.counts.end()) ? it->second : 0;
|
||||
if (have < ing.amount)
|
||||
{
|
||||
inputsOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inputsOk)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Production prod;
|
||||
prod.recipeId = building.recipeId;
|
||||
prod.completesAt = currentTick + secondsToTicks(recipe->durationSeconds);
|
||||
prod.chosenOutputs = std::move(chosen);
|
||||
building.production = std::move(prod);
|
||||
// 2. Determine chosen outputs (roll for reprocessing).
|
||||
std::vector<Item> chosen;
|
||||
if (building.type == BuildingType::ReprocessingPlant)
|
||||
{
|
||||
chosen = rollReprocessingOutput(*recipe);
|
||||
if (chosen.empty()) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const RecipeOutput& out : recipe->outputs)
|
||||
{
|
||||
Item item;
|
||||
item.type.id = out.item;
|
||||
for (int i = 0; i < out.amount; ++i)
|
||||
{
|
||||
chosen.push_back(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Output buffer has space for chosen outputs?
|
||||
const int newSize = static_cast<int>(building.outputBuffer.items.size())
|
||||
+ static_cast<int>(chosen.size());
|
||||
if (newSize > building.outputBuffer.capacity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Consume inputs and start cycle.
|
||||
for (const RecipeIngredient& ing : recipe->inputs)
|
||||
{
|
||||
building.inputBuffer.counts[ItemType{ing.item}] -= ing.amount;
|
||||
}
|
||||
|
||||
Production prod;
|
||||
prod.recipeId = recipe->id;
|
||||
prod.completesAt = currentTick + secondsToTicks(recipe->durationSeconds);
|
||||
prod.chosenOutputs = std::move(chosen);
|
||||
building.production = std::move(prod);
|
||||
break; // At most one cycle starts per tick.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,10 @@ private:
|
||||
const ShipDef* findShipDef(const std::string& id) const;
|
||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
||||
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
||||
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
|
||||
// caps span the union of every recipe of the building's type; no player
|
||||
// recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||
void initAutoBuffers(Building& b) const;
|
||||
void initShipyardBuffers(Building& b) const;
|
||||
void initSalvageBayBuffer(Building& b) const;
|
||||
std::vector<Port> computeInputPorts(const Building& b) const;
|
||||
|
||||
@@ -474,7 +474,8 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
|
||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)), tick);
|
||||
REQUIRE(bs.productionBuildingCount() == 2);
|
||||
|
||||
// Neither has a recipe selected, so neither has an active cycle.
|
||||
// Neither is producing yet: the miner has no recipe selected, and the
|
||||
// smelter (auto-recipe, REQ-BLD-SMELTER) has no input feeding it.
|
||||
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
||||
|
||||
bs.setRecipe(minerId, "mine_iron_ore");
|
||||
@@ -542,7 +543,8 @@ 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 = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
|
||||
bs.setRecipe(sid, "iron_ingot");
|
||||
// Smelters have no recipe selection (REQ-BLD-SMELTER); they auto-accept any
|
||||
// ore/scrap that is an input to a smelter recipe.
|
||||
|
||||
// Complete construction (15s → tick 450+1 = 451 ticks).
|
||||
Tick tick = 0;
|
||||
@@ -563,6 +565,105 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
|
||||
REQUIRE(it->second >= 1);
|
||||
}
|
||||
|
||||
// A smelter auto-selects the matching recipe for whatever it is fed, with no
|
||||
// player recipe selection (REQ-BLD-SMELTER).
|
||||
TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
BuildingId nextBuildingId = 1;
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextBuildingId]() { return nextBuildingId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||
[](const std::string&) -> bool { return true; },
|
||||
rng);
|
||||
|
||||
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
|
||||
|
||||
Tick tick = 0;
|
||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
|
||||
|
||||
// Feed 2 iron_ore (the test-config iron_ingot recipe needs 2) via a
|
||||
// west-flowing belt at input port (2,0).
|
||||
belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore"));
|
||||
belts.tick();
|
||||
bs.tickBeltPull();
|
||||
}
|
||||
|
||||
// iron_ingot recipe cycle is 2s; run to completion.
|
||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(2.0)) + 2, tick);
|
||||
|
||||
const Building* b = bs.findBuilding(sid);
|
||||
REQUIRE(b != nullptr);
|
||||
bool hasIronIngot = false;
|
||||
for (const Item& item : b->outputBuffer.items)
|
||||
{
|
||||
if (item.type.id == "iron_ingot") { hasIronIngot = true; }
|
||||
}
|
||||
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",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
BeltSystem belts(static_cast<double>(kTickRateHz));
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
BuildingId nextBuildingId = 1;
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextBuildingId]() { return nextBuildingId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||
[](const std::string&) -> bool { return true; },
|
||||
rng);
|
||||
|
||||
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
|
||||
|
||||
Tick tick = 0;
|
||||
runTicks(bs, belts, 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.
|
||||
belts.placeBelt(QPoint(2, 0), Rotation::West);
|
||||
const char* fed[] = { "iron_ore", "copper_ore", "copper_ore" };
|
||||
for (const char* id : fed)
|
||||
{
|
||||
belts.tryPutItem(QPoint(2, 0), makeItem(id));
|
||||
belts.tick();
|
||||
bs.tickBeltPull();
|
||||
}
|
||||
|
||||
// copper_ingot cycle is 2.5s; run to completion.
|
||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(2.5)) + 2, tick);
|
||||
|
||||
const Building* b = bs.findBuilding(sid);
|
||||
REQUIRE(b != nullptr);
|
||||
|
||||
// Copper was smelted; the lone iron_ore still waits for a second unit.
|
||||
bool hasCopperIngot = false;
|
||||
for (const Item& item : b->outputBuffer.items)
|
||||
{
|
||||
if (item.type.id == "copper_ingot") { hasCopperIngot = true; }
|
||||
}
|
||||
REQUIRE(hasCopperIngot);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Belt push → belt tile
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -664,7 +765,8 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
|
||||
|
||||
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
||||
QPoint(0, 0), Rotation::East, 0);
|
||||
bs.setRecipe(id, "reprocessing_cycle");
|
||||
// Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the
|
||||
// single reprocessing recipe is applied automatically on completion.
|
||||
|
||||
// Complete construction (25s).
|
||||
Tick tick = 0;
|
||||
@@ -695,7 +797,8 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
|
||||
|
||||
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
|
||||
QPoint(0, 0), Rotation::East, 0);
|
||||
bs.setRecipe(id, "reprocessing_cycle");
|
||||
// Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the
|
||||
// single reprocessing recipe is applied automatically on completion.
|
||||
|
||||
// Complete construction (25s).
|
||||
Tick tick = 0;
|
||||
|
||||
@@ -78,6 +78,25 @@ bool isProductionBuilding(BuildingType type)
|
||||
|| type == BuildingType::Shipyard;
|
||||
}
|
||||
|
||||
// Buildings that expose a player recipe/schematic selection control
|
||||
// (REQ-UI-SELECT-BUTTON): Miner ore type, Assembler recipe, Shipyard schematic.
|
||||
// The Smelter and Reprocessing Plant auto-process and offer no selection
|
||||
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||
bool hasRecipeSelection(BuildingType type)
|
||||
{
|
||||
return type == BuildingType::Miner
|
||||
|| type == BuildingType::Assembler
|
||||
|| type == BuildingType::Shipyard;
|
||||
}
|
||||
|
||||
// Auto-recipe buildings have no selected recipe; their production is driven by
|
||||
// whatever inputs they receive.
|
||||
bool isAutoRecipeBuilding(BuildingType type)
|
||||
{
|
||||
return type == BuildingType::Smelter
|
||||
|| type == BuildingType::ReprocessingPlant;
|
||||
}
|
||||
|
||||
bool isBeltLike(BuildingType type)
|
||||
{
|
||||
return type == BuildingType::Belt || type == BuildingType::Splitter
|
||||
@@ -265,7 +284,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
||||
m_titleLabel->show();
|
||||
m_buffersLabel->show();
|
||||
|
||||
if (isProductionBuilding(type))
|
||||
if (hasRecipeSelection(type))
|
||||
{
|
||||
const std::vector<RecipeSelectionOption> options =
|
||||
buildRecipeSelectionOptions(type, *m_sim, *m_config);
|
||||
@@ -383,6 +402,21 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
? findShipDef(b->recipeId)
|
||||
: nullptr;
|
||||
|
||||
// Auto-recipe buildings (Smelter, Reprocessing Plant) have no selected
|
||||
// recipe; while a cycle runs, resolve the recipe actually in production so
|
||||
// the cycle time and progress can be shown (REQ-UI-PRODUCTION-PROGRESS).
|
||||
if (!recipe && isAutoRecipeBuilding(b->type) && b->production.has_value())
|
||||
{
|
||||
for (const RecipeDef& r : m_config->recipes.recipes)
|
||||
{
|
||||
if (r.id == b->production->recipeId && r.building == b->type)
|
||||
{
|
||||
recipe = &r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString bufText;
|
||||
|
||||
if (!b->inputBuffer.counts.empty())
|
||||
@@ -469,41 +503,51 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
||||
}
|
||||
}
|
||||
|
||||
if (isProductionBuilding(b->type) && (recipe || shipDef))
|
||||
if (isProductionBuilding(b->type)
|
||||
&& (recipe || shipDef || isAutoRecipeBuilding(b->type)))
|
||||
{
|
||||
double durationSeconds = recipe
|
||||
? recipe->durationSeconds
|
||||
: shipDef->schematic.productionTimeSeconds;
|
||||
|
||||
if (shipDef && b->shipLayout.has_value())
|
||||
if (recipe || shipDef)
|
||||
{
|
||||
for (const PlacedModule& pm : b->shipLayout->placedModules)
|
||||
double durationSeconds = recipe
|
||||
? recipe->durationSeconds
|
||||
: shipDef->schematic.productionTimeSeconds;
|
||||
|
||||
if (shipDef && b->shipLayout.has_value())
|
||||
{
|
||||
for (const ModuleDef& modDef : m_config->modules.modules)
|
||||
for (const PlacedModule& pm : b->shipLayout->placedModules)
|
||||
{
|
||||
if (modDef.id == pm.moduleId)
|
||||
for (const ModuleDef& modDef : m_config->modules.modules)
|
||||
{
|
||||
durationSeconds += modDef.productionTimeSeconds;
|
||||
break;
|
||||
if (modDef.id == pm.moduleId)
|
||||
{
|
||||
durationSeconds += modDef.productionTimeSeconds;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bufText += tr("Cycle: %1 s\n").arg(durationSeconds, 0, 'f', 1);
|
||||
bufText += tr("Cycle: %1 s\n").arg(durationSeconds, 0, 'f', 1);
|
||||
|
||||
if (b->production.has_value())
|
||||
{
|
||||
const Tick cycleTicks = secondsToTicks(durationSeconds);
|
||||
const Tick completesAt = b->production->completesAt;
|
||||
const Tick currentTick = m_sim->currentTick();
|
||||
const Tick elapsed = currentTick - (completesAt - cycleTicks);
|
||||
const int pct = static_cast<int>(
|
||||
std::max(Tick(0), std::min(cycleTicks, elapsed)) * 100 / cycleTicks);
|
||||
bufText += tr("Progress: %1%\n").arg(pct);
|
||||
if (b->production.has_value())
|
||||
{
|
||||
const Tick cycleTicks = secondsToTicks(durationSeconds);
|
||||
const Tick completesAt = b->production->completesAt;
|
||||
const Tick currentTick = m_sim->currentTick();
|
||||
const Tick elapsed = currentTick - (completesAt - cycleTicks);
|
||||
const int pct = static_cast<int>(
|
||||
std::max(Tick(0), std::min(cycleTicks, elapsed)) * 100 / cycleTicks);
|
||||
bufText += tr("Progress: %1%\n").arg(pct);
|
||||
}
|
||||
else
|
||||
{
|
||||
bufText += tr("Progress: idle\n");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Auto-recipe building with no active cycle: no single recipe to
|
||||
// show a cycle time for.
|
||||
bufText += tr("Progress: idle\n");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user