Implements REQ-MAT-OUTPUT-GROUP. A recipe had two shapes -- outputs produced together, or outputs of which exactly one happened -- and every rule over them was written twice, selected by `building == ReprocessingPlant`: sizing a buffer, deciding whether a cycle fits, resolving what a cycle makes, costing an item. RecipeDef now holds output groups, each a weight and a list of items, and a cycle yields exactly one group. One group is the ordinary recipe, so the old two cases are the same shape with one and with several, and all four rules collapse to one expression apiece with no building-type test left. rollReprocessingOutput becomes rollOutputGroup, where a single group returns without drawing or testing eligibility. That early-out is load-bearing twice over. Drawing there would consume entropy for every ordinary recipe and shift every later random outcome; and eligibility must not apply either, since implicit unlocking is demand-derived, so an ordinary recipe's output can be producible while nothing yet calls for it -- testing it would stop the building producing rather than gate a drop. Past the early-out a group is eligible only when all of its items are unlocked, being produced whole. Threat follows the recipe's shape rather than the building, and the per-unit value now divides by the group's amount as well as its odds. That moves no number today: every item resolved through this path has amount 1, which is why the threat expectations are untouched. Config keeps `outputs = [...]` as the single-group form, so only the two reprocessing recipes change shape. The recipe summary gains "/" between groups and keeps "+" within one, which also fixes the plant reading as though a cycle produced all of its items at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
230 lines
8.3 KiB
C++
230 lines
8.3 KiB
C++
#include "BufferedBuildingContent.h"
|
|
|
|
#include <map>
|
|
#include <set>
|
|
|
|
#include <QVBoxLayout>
|
|
|
|
#include "Building.h"
|
|
#include "BuildingTarget.h"
|
|
#include "DisplayName.h"
|
|
#include "FactoryQueries.h"
|
|
#include "ProductionSection.h"
|
|
#include "RecipeLineRow.h"
|
|
#include "SectionBox.h"
|
|
#include "SelectionNames.h"
|
|
#include "Simulation.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
std::vector<RecipeLineRow::Amount> toAmounts(const std::map<std::string, int>& map)
|
|
{
|
|
std::vector<RecipeLineRow::Amount> amounts;
|
|
amounts.reserve(map.size());
|
|
for (const std::pair<const std::string, int>& entry : map)
|
|
{
|
|
amounts.push_back(RecipeLineRow::Amount{ entry.first, entry.second });
|
|
}
|
|
return amounts;
|
|
}
|
|
|
|
// Every item one side of the card should list: what the buffer holds, what a cycle
|
|
// moves, and what the building handles at all. A building's buffers can carry items it
|
|
// is not currently making anything of, and an auto-recipe building between cycles names
|
|
// nothing at all, so the three sources are unioned rather than one being picked.
|
|
std::set<std::string> collectItemIds(const std::map<std::string, int>& buffered,
|
|
const std::map<std::string, int>& perCycle,
|
|
const std::vector<std::string>& handled)
|
|
{
|
|
std::set<std::string> itemIds;
|
|
for (const std::pair<const std::string, int>& entry : buffered)
|
|
{
|
|
itemIds.insert(entry.first);
|
|
}
|
|
for (const std::pair<const std::string, int>& entry : perCycle)
|
|
{
|
|
itemIds.insert(entry.first);
|
|
}
|
|
itemIds.insert(handled.begin(), handled.end());
|
|
return itemIds;
|
|
}
|
|
|
|
int lookUp(const std::map<std::string, int>& map, const std::string& key)
|
|
{
|
|
const std::map<std::string, int>::const_iterator it = map.find(key);
|
|
return (it != map.end()) ? it->second : 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context,
|
|
BuildingId id, QWidget* parent)
|
|
: SelectionContent(context, asConstructionSite(context, id), parent)
|
|
, m_id(id)
|
|
{
|
|
// The summary is configuration -- what the building will do -- so it sits with the
|
|
// selection control and is shown for a construction site too (REQ-UI-RECIPE-SUMMARY).
|
|
m_recipeSummary = new RecipeLineRow(context.itemIcons, context.buildingIcons, this);
|
|
getConfigurationLayout()->addWidget(m_recipeSummary);
|
|
|
|
m_inputSection = new SectionBox(tr("Input buffers"), this);
|
|
m_inputChips = new ItemChipRow(context, m_inputSection);
|
|
m_inputSection->getContentLayout()->addWidget(m_inputChips);
|
|
|
|
m_production = new ProductionSection(this);
|
|
|
|
m_outputSection = new SectionBox(tr("Output buffers"), this);
|
|
m_outputChips = new ItemChipRow(context, m_outputSection);
|
|
m_outputSection->getContentLayout()->addWidget(m_outputChips);
|
|
|
|
// In the direction the materials flow: what goes in, what is being made of it, what
|
|
// has come out (REQ-UI-SINGLE-SELECTION, REQ-UI-PRODUCTION-PROGRESS).
|
|
getRuntimeLayout()->addWidget(m_inputSection);
|
|
getRuntimeLayout()->addWidget(m_production);
|
|
getRuntimeLayout()->addWidget(m_outputSection);
|
|
}
|
|
|
|
void BufferedBuildingContent::refreshConfiguration()
|
|
{
|
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
|
if (!target.isValid())
|
|
{
|
|
// Gone under the card. SelectionPanel rebuilds on the same refresh; this only
|
|
// has to avoid reading it.
|
|
return;
|
|
}
|
|
|
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
|
|
|
// The card already names the building in its header, so the summary states the cycle
|
|
// alone -- no building chip, no recipe name (REQ-UI-RECIPE-SUMMARY).
|
|
const CycleInfo cycle = getCycleInfo(target);
|
|
RecipeLineRow::Spec summary;
|
|
summary.inputs = toAmounts(cycle.perCycleInputs);
|
|
summary.outputGroups = cycle.perCycleOutputGroups;
|
|
summary.durationSeconds = cycle.durationSeconds;
|
|
m_recipeSummary->setLine(summary);
|
|
|
|
refreshControls(target);
|
|
}
|
|
|
|
void BufferedBuildingContent::refreshRuntime()
|
|
{
|
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
|
if (!target.building)
|
|
{
|
|
return;
|
|
}
|
|
|
|
setProductionStatusSlot(*target.building);
|
|
|
|
const CycleInfo cycle = getCycleInfo(target);
|
|
|
|
const std::vector<ItemChipRow::Entry> inputs =
|
|
buildInputEntries(*target.building, cycle);
|
|
const std::vector<ItemChipRow::Entry> outputs =
|
|
buildOutputEntries(*target.building, cycle);
|
|
|
|
m_inputChips->setEntries(inputs);
|
|
m_outputChips->setEntries(outputs);
|
|
m_inputSection->setVisible(!inputs.empty());
|
|
m_outputSection->setVisible(!outputs.empty());
|
|
|
|
m_production->setProduction(cycle.runsProduction, *target.building,
|
|
cycle.durationSeconds,
|
|
getContext().sim->getCurrentTick());
|
|
}
|
|
|
|
std::vector<ItemChipRow::Entry> BufferedBuildingContent::buildInputEntries(
|
|
const Building& building, const CycleInfo& cycle) const
|
|
{
|
|
std::map<std::string, int> buffered;
|
|
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
|
|
{
|
|
buffered[entry.first.id] = entry.second;
|
|
}
|
|
|
|
std::vector<ItemChipRow::Entry> entries;
|
|
for (const std::string& itemId :
|
|
collectItemIds(buffered, cycle.perCycleInputs, cycle.handledInputs))
|
|
{
|
|
// An auto-recipe building's buffers are sized over every recipe of its type,
|
|
// including recipes still locked, so those entries are left out here
|
|
// (REQ-UI-SINGLE-SELECTION, REQ-LOCK-UI-RECIPE).
|
|
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
|
|
|
|
ItemChipRow::Entry chip;
|
|
chip.itemId = itemId;
|
|
chip.countText = QString::number(lookUp(buffered, itemId));
|
|
|
|
const int perCycle = lookUp(cycle.perCycleInputs, itemId);
|
|
if (perCycle > 0)
|
|
{
|
|
chip.subLine = tr("/ %1 per cycle").arg(perCycle);
|
|
}
|
|
else
|
|
{
|
|
chip.subLine = QString::fromStdString(toDisplayName(itemId));
|
|
}
|
|
entries.push_back(chip);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
std::vector<ItemChipRow::Entry> BufferedBuildingContent::buildOutputEntries(
|
|
const Building& building, const CycleInfo& cycle) const
|
|
{
|
|
// The buffered items plus those still emerging onto the output belts: an emerging
|
|
// item still belongs to the output buffer (REQ-MAT-OUTPUT-EMERGE), so leaving it out
|
|
// would make it vanish from the panel while it animates.
|
|
std::map<std::string, int> buffered;
|
|
for (const Item& item : building.outputBuffer.items)
|
|
{
|
|
buffered[item.type.id]++;
|
|
}
|
|
for (const std::vector<BeltItemSlot>& lane : building.emergingItems)
|
|
{
|
|
for (const BeltItemSlot& slot : lane)
|
|
{
|
|
buffered[slot.item.type.id]++;
|
|
}
|
|
}
|
|
|
|
// A chip stands for a buffer, and a buffer exists for every item any group can
|
|
// produce (REQ-MAT-OUTPUT-BUFFER), so the groups are flattened here.
|
|
std::map<std::string, int> producible;
|
|
for (const std::vector<RecipeLineRow::Amount>& group : cycle.perCycleOutputGroups)
|
|
{
|
|
for (const RecipeLineRow::Amount& amount : group)
|
|
{
|
|
producible[amount.itemId] = std::max(producible[amount.itemId], amount.amount);
|
|
}
|
|
}
|
|
|
|
std::vector<ItemChipRow::Entry> entries;
|
|
for (const std::string& itemId :
|
|
collectItemIds(buffered, producible, cycle.handledOutputs))
|
|
{
|
|
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
|
|
|
|
ItemChipRow::Entry chip;
|
|
chip.itemId = itemId;
|
|
// Counted against this item's own buffer capacity, which is what production
|
|
// stops at (REQ-MAT-OUTPUT-BUFFER, REQ-UI-SINGLE-SELECTION). A chip for an item
|
|
// the building has no buffer for -- one left over from a previous recipe --
|
|
// carries the bare count, as an unsized buffer has no denominator to state.
|
|
const std::map<ItemType, int>::const_iterator capIt =
|
|
building.outputBuffer.caps.find(ItemType{itemId});
|
|
const int cap =
|
|
(capIt != building.outputBuffer.caps.end()) ? capIt->second : 0;
|
|
chip.countText = cap > 0
|
|
? tr("%1 / %2").arg(lookUp(buffered, itemId)).arg(cap)
|
|
: QString::number(lookUp(buffered, itemId));
|
|
chip.subLine = QString::fromStdString(toDisplayName(itemId));
|
|
entries.push_back(chip);
|
|
}
|
|
return entries;
|
|
}
|