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
224 lines
7.2 KiB
C++
224 lines
7.2 KiB
C++
#include "RecipeLineRow.h"
|
|
|
|
#include <QFont>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QLayoutItem>
|
|
#include <QMargins>
|
|
#include <QPixmap>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "BuildingIconCache.h"
|
|
#include "ItemIconCache.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
// Size the items' colored squares and the building's chip are drawn at on a line, in
|
|
// device-independent pixels. Larger than the artwork they carry, which the square
|
|
// insets (REQ-UI-ITEM-ICON).
|
|
const int kIconSizePx = 18;
|
|
|
|
// Adds a freshly built label to the line and shows it.
|
|
//
|
|
// The show is what makes it count: a widget created under an already-visible parent
|
|
// starts hidden, and a layout treats a hidden item as empty, so an unshown label would
|
|
// add nothing to the size hint the surrounding widget measures itself against as soon
|
|
// as this returns.
|
|
void addAndShow(QHBoxLayout* layout, QLabel* label)
|
|
{
|
|
layout->addWidget(label);
|
|
label->show();
|
|
}
|
|
|
|
// Empties one of the two rows, leaving the row widget and its layout in place.
|
|
void clearRow(QHBoxLayout* layout)
|
|
{
|
|
while (QLayoutItem* item = layout->takeAt(0))
|
|
{
|
|
if (item->widget())
|
|
{
|
|
item->widget()->deleteLater();
|
|
}
|
|
delete item;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
std::vector<std::vector<RecipeLineRow::Amount>> RecipeLineRow::toOutputGroups(
|
|
const RecipeDef& recipe)
|
|
{
|
|
std::vector<std::vector<Amount>> groups;
|
|
groups.reserve(recipe.outputGroups.size());
|
|
for (const RecipeOutputGroup& group : recipe.outputGroups)
|
|
{
|
|
std::vector<Amount> amounts;
|
|
amounts.reserve(group.items.size());
|
|
for (const RecipeOutput& out : group.items)
|
|
{
|
|
amounts.push_back(Amount{ out.item, out.amount });
|
|
}
|
|
groups.push_back(std::move(amounts));
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
RecipeLineRow::RecipeLineRow(ItemIconCache* itemIcons, BuildingIconCache* buildingIcons,
|
|
QWidget* parent)
|
|
: QWidget(parent)
|
|
, m_itemIcons(itemIcons)
|
|
, m_buildingIcons(buildingIcons)
|
|
{
|
|
m_outerLayout = new QVBoxLayout(this);
|
|
m_outerLayout->setContentsMargins(0, 0, 0, 0);
|
|
m_outerLayout->setSpacing(1);
|
|
|
|
m_headerRow = new QWidget(this);
|
|
m_headerLayout = new QHBoxLayout(m_headerRow);
|
|
m_headerLayout->setContentsMargins(0, 0, 0, 0);
|
|
m_headerLayout->setSpacing(4);
|
|
m_headerRow->hide();
|
|
m_outerLayout->addWidget(m_headerRow);
|
|
|
|
m_amountsRow = new QWidget(this);
|
|
m_amountsLayout = new QHBoxLayout(m_amountsRow);
|
|
m_amountsLayout->setContentsMargins(0, 0, 0, 0);
|
|
m_amountsLayout->setSpacing(4);
|
|
m_outerLayout->addWidget(m_amountsRow);
|
|
|
|
hide();
|
|
}
|
|
|
|
void RecipeLineRow::setCardChrome(bool enabled)
|
|
{
|
|
// Palette colors like the item chip's chrome (REQ-UI-SELECTION-CARD), not
|
|
// visuals.toml, which is for world rendering. The selector names this class alone, so
|
|
// nothing inside the card inherits the border.
|
|
setAttribute(Qt::WA_StyledBackground, enabled);
|
|
setStyleSheet(enabled
|
|
? QStringLiteral("RecipeLineRow { border: 1px solid palette(mid); "
|
|
"border-radius: 4px; }")
|
|
: QString());
|
|
m_outerLayout->setContentsMargins(enabled ? QMargins(6, 4, 6, 4)
|
|
: QMargins(0, 0, 0, 0));
|
|
}
|
|
|
|
void RecipeLineRow::setLine(const Spec& spec)
|
|
{
|
|
if (spec.isEmpty())
|
|
{
|
|
// Forgotten as well as hidden, so re-selecting the same recipe later is seen as
|
|
// a change and shows the row again.
|
|
m_spec = Spec();
|
|
hide();
|
|
return;
|
|
}
|
|
if (spec == m_spec)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_spec = spec;
|
|
rebuild(spec);
|
|
show();
|
|
}
|
|
|
|
void RecipeLineRow::rebuild(const Spec& spec)
|
|
{
|
|
clearRow(m_headerLayout);
|
|
clearRow(m_amountsLayout);
|
|
|
|
// First line: which building runs the recipe and which recipe it is, where the line
|
|
// has to distinguish one producer from another (REQ-UI-ITEM-TOOLTIP). A building with
|
|
// no chip file leaves the icon off, as everywhere else (REQ-UI-BUILD-ICON).
|
|
if (spec.building.has_value() && m_buildingIcons != nullptr)
|
|
{
|
|
const QPixmap chip =
|
|
m_buildingIcons->getChip(buildingTypeId(*spec.building), kIconSizePx);
|
|
if (!chip.isNull())
|
|
{
|
|
QLabel* chipLabel = new QLabel(m_headerRow);
|
|
chipLabel->setPixmap(chip);
|
|
addAndShow(m_headerLayout, chipLabel);
|
|
}
|
|
}
|
|
if (!spec.name.isEmpty())
|
|
{
|
|
QLabel* nameLabel = new QLabel(spec.name, m_headerRow);
|
|
QFont nameFont = nameLabel->font();
|
|
nameFont.setBold(true);
|
|
nameLabel->setFont(nameFont);
|
|
addAndShow(m_headerLayout, nameLabel);
|
|
}
|
|
const bool hasHeader = m_headerLayout->count() > 0;
|
|
if (hasHeader)
|
|
{
|
|
m_headerLayout->addStretch(1);
|
|
}
|
|
m_headerRow->setVisible(hasHeader);
|
|
|
|
// Second line: what the cycle costs, makes and takes.
|
|
addAmounts(spec.inputs);
|
|
if (!spec.inputs.empty() && !spec.outputGroups.empty())
|
|
{
|
|
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
|
addAndShow(m_amountsLayout, new QLabel(QString(rightArrow), m_amountsRow));
|
|
}
|
|
for (std::size_t i = 0; i < spec.outputGroups.size(); ++i)
|
|
{
|
|
// Between one group and the next, so alternatives read as a choice rather than as
|
|
// one combined yield -- which is what a run of `+` would say (REQ-UI-RECIPE-SUMMARY).
|
|
if (i > 0)
|
|
{
|
|
addAndShow(m_amountsLayout, new QLabel(QStringLiteral("/"), m_amountsRow));
|
|
}
|
|
addAmounts(spec.outputGroups[i]);
|
|
}
|
|
|
|
if (spec.durationSeconds.has_value() && *spec.durationSeconds > 0.0)
|
|
{
|
|
const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT
|
|
const QString time = spec.durationIsAddition
|
|
? tr("+%1 s").arg(*spec.durationSeconds, 0, 'f', 1)
|
|
: tr("%1 s").arg(*spec.durationSeconds, 0, 'f', 1);
|
|
addAndShow(m_amountsLayout, new QLabel(
|
|
QStringLiteral("%1 %2").arg(middleDot).arg(time), m_amountsRow));
|
|
}
|
|
m_amountsLayout->addStretch(1);
|
|
}
|
|
|
|
void RecipeLineRow::addAmounts(const std::vector<Amount>& amounts)
|
|
{
|
|
for (const Amount& entry : amounts)
|
|
{
|
|
// Between one item and the next, so a run of icons and numbers reads as a sum
|
|
// rather than as a list.
|
|
if (&entry != &amounts.front())
|
|
{
|
|
addAndShow(m_amountsLayout, new QLabel(QStringLiteral("+"), m_amountsRow));
|
|
}
|
|
|
|
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
|
|
// is not an error: the square stands alone then, and only an item with no square
|
|
// either falls back to its id in text.
|
|
const QPixmap icon = (m_itemIcons != nullptr)
|
|
? m_itemIcons->getSquarePixmap(entry.itemId, kIconSizePx)
|
|
: QPixmap();
|
|
if (!icon.isNull())
|
|
{
|
|
QLabel* iconLabel = new QLabel(m_amountsRow);
|
|
iconLabel->setPixmap(icon);
|
|
addAndShow(m_amountsLayout, iconLabel);
|
|
}
|
|
else
|
|
{
|
|
addAndShow(m_amountsLayout,
|
|
new QLabel(QString::fromStdString(entry.itemId), m_amountsRow));
|
|
}
|
|
addAndShow(m_amountsLayout,
|
|
new QLabel(QString::number(entry.amount), m_amountsRow));
|
|
}
|
|
}
|