give a recipe line its own row for its identity

This commit is contained in:
2026-08-11 22:07:15 +02:00
parent 1ddd830476
commit a3fddf63cc
4 changed files with 80 additions and 38 deletions

View File

@@ -373,7 +373,7 @@ Any ship, module, building, or assembler recipe id that appears in no unlock gro
Each option additionally displays a vertical list of recipe lines labeled "Unlocks recipes:", showing which miner and assembler recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the miner recipes and implicitly-gated assembler recipes that are not currently implicitly unlocked but would become so after applying this option's effect. To compute this, all `materials` of the group's granted ship and module schematics are added to the base set per REQ-LOCK-IMPLICIT step 1a, and the output items of the group's granted assembler recipes are added per step 1b, before recomputation. Each option additionally displays a vertical list of recipe lines labeled "Unlocks recipes:", showing which miner and assembler recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the miner recipes and implicitly-gated assembler recipes that are not currently implicitly unlocked but would become so after applying this option's effect. To compute this, all `materials` of the group's granted ship and module schematics are added to the base set per REQ-LOCK-IMPLICIT step 1a, and the output items of the group's granted assembler recipes are added per step 1b, before recomputation.
Each recipe is shown as a **recipe line** of the same form the item production tooltip uses (REQ-UI-ITEM-TOOLTIP): the icon of the building that runs it (REQ-UI-BUILD-ICON), the recipe's name — by its `id`, using the same display convention as the assembler recipe-selection dialog — and the recipe drawn as the recipe summary line draws it (REQ-UI-RECIPE-SUMMARY). The lines are sorted alphabetically by recipe name. The line says everything there is to say about the recipe, so nothing in this list carries a tooltip, as in the selection dialog (REQ-UI-SELECT-OPTIONS). If no recipes would be newly unlocked, the list shows "None". Each recipe is shown as a **recipe line** of the same two-row form the item production tooltip uses (REQ-UI-ITEM-TOOLTIP): the icon of the building that runs it (REQ-UI-BUILD-ICON) and the recipe's name — by its `id`, using the same display convention as the assembler recipe-selection dialog — on the first row, and the recipe drawn as the recipe summary draws it (REQ-UI-RECIPE-SUMMARY) on the second. The lines are sorted alphabetically by recipe name. The line says everything there is to say about the recipe, so nothing in this list carries a tooltip, as in the selection dialog (REQ-UI-SELECT-OPTIONS). If no recipes would be newly unlocked, the list shows "None".
The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no unlock is applied. Otherwise the selected unlock group is awarded and the dialog closes: every ship, module, building, and assembler recipe the group grants becomes unlocked at once — ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG); building types become available in the build menu (REQ-LOCK-BUILDING); assembler recipes become available in the assembler recipe-selection dialog (subject to REQ-LOCK-UI-RECIPE). The unlock group is removed from the pool permanently (REQ-LOCK-EXPLICIT), and the implicit unlock set is recomputed (REQ-LOCK-IMPLICIT). The player selects one option by clicking it. If the player selects the artifact option, the player's artifact count is incremented by 1 (REQ-WIN-ARTIFACT-COUNT) and the dialog closes; no unlock is applied. Otherwise the selected unlock group is awarded and the dialog closes: every ship, module, building, and assembler recipe the group grants becomes unlocked at once — ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG); building types become available in the build menu (REQ-LOCK-BUILDING); assembler recipes become available in the assembler recipe-selection dialog (subject to REQ-LOCK-UI-RECIPE). The unlock group is removed from the pool permanently (REQ-LOCK-EXPLICIT), and the implicit unlock set is recomputed (REQ-LOCK-IMPLICIT).
@@ -600,7 +600,7 @@ The panel shows exactly one **content** at a time, picked from the catalog in RE
- The `(None)` option shows its text caption alone. - The `(None)` option shows its text caption alone.
- REQ-UI-ITEM-TOOLTIP: **Item production tooltip.** Hovering an item chip in the selection panel — an input or output buffer chip (REQ-UI-SINGLE-SELECTION) or the HQ's block stock chip (REQ-UI-HQ-PANEL) — shows a tooltip telling the player where that item comes from. It has a heading and a body: - REQ-UI-ITEM-TOOLTIP: **Item production tooltip.** Hovering an item chip in the selection panel — an input or output buffer chip (REQ-UI-SINGLE-SELECTION) or the HQ's block stock chip (REQ-UI-HQ-PANEL) — shows a tooltip telling the player where that item comes from. It has a heading and a body:
- the heading is the **hovered item's name**. For an input chip this is the only place the item is named at all, since such a chip carries a count and no name (REQ-UI-SINGLE-SELECTION). - the heading is the **hovered item's name**. For an input chip this is the only place the item is named at all, since such a chip carries a count and no name (REQ-UI-SINGLE-SELECTION).
- the body is the caption `Produced by` followed by one **recipe line** per unlocked recipe that produces the item. Each line carries the icon of the building that runs that recipe (REQ-UI-BUILD-ICON), the recipe's name, and the recipe drawn as the recipe summary line draws it (REQ-UI-RECIPE-SUMMARY): each input item's icon on its colored square with its per-cycle amount, an arrow, each output item's icon with its amount, and the cycle time. An item may have several producers — an iron ingot is smelted from ore, smelted from scrap, and recovered by reprocessing — and the building icon and recipe name are what tell those lines apart and tell the player which building to place for which path. - the body is the caption `Produced by` followed by one **recipe line** per unlocked recipe that produces the item. A recipe line takes two rows: the icon of the building that runs the recipe (REQ-UI-BUILD-ICON) and the recipe's name on the first, and the recipe itself on the second, drawn as the recipe summary draws it (REQ-UI-RECIPE-SUMMARY) each input item's icon on its colored square with its per-cycle amount, an arrow, each output item's icon with its amount, and the cycle time. Two rows rather than one because an identity and a cycle read as different things, and a single row of icons, names and numbers runs too long to scan. An item may have several producers — an iron ingot is smelted from ore, smelted from scrap, and recovered by reprocessing — and the building icon and recipe name are what tell those lines apart and tell the player which building to place for which path.
**Only recipes the player can run are listed**, consistent with the rest of the UI hiding what the player cannot make yet. What that means differs by building, because only Miner and Assembler recipes are unlocked individually (REQ-LOCK-UI-RECIPE): those are listed once unlocked, while a Smelter's or Reprocessing Plant's implicit recipes (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) are listed once **their building** is unlocked (REQ-LOCK-BUILDING) — there is no sense in naming a path through a plant the player cannot place. Two cases have no recipe line to show, and each says so in place of the list rather than leaving the tooltip bare: **Only recipes the player can run are listed**, consistent with the rest of the UI hiding what the player cannot make yet. What that means differs by building, because only Miner and Assembler recipes are unlocked individually (REQ-LOCK-UI-RECIPE): those are listed once unlocked, while a Smelter's or Reprocessing Plant's implicit recipes (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING) are listed once **their building** is unlocked (REQ-LOCK-BUILDING) — there is no sense in naming a path through a plant the player cannot place. Two cases have no recipe line to show, and each says so in place of the list rather than leaving the tooltip bare:
- **An item no recipe produces at all.** Scrap is salvaged from debris (REQ-RES-DEBRIS-DROP) rather than crafted, so its tooltip reads `Salvaged from debris` in place of the `Produced by` caption and lists nothing beneath it. - **An item no recipe produces at all.** Scrap is salvaged from debris (REQ-RES-DEBRIS-DROP) rather than crafted, so its tooltip reads `Salvaged from debris` in place of the `Produced by` caption and lists nothing beneath it.

View File

@@ -4,6 +4,7 @@
#include <QLabel> #include <QLabel>
#include <QLayoutItem> #include <QLayoutItem>
#include <QPixmap> #include <QPixmap>
#include <QVBoxLayout>
#include "BuildingIconCache.h" #include "BuildingIconCache.h"
#include "ItemIconCache.h" #include "ItemIconCache.h"
@@ -28,6 +29,19 @@ void addAndShow(QHBoxLayout* layout, QLabel* label)
label->show(); 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 } // namespace
@@ -37,9 +51,22 @@ RecipeLineRow::RecipeLineRow(ItemIconCache* itemIcons, BuildingIconCache* buildi
, m_itemIcons(itemIcons) , m_itemIcons(itemIcons)
, m_buildingIcons(buildingIcons) , m_buildingIcons(buildingIcons)
{ {
m_layout = new QHBoxLayout(this); QVBoxLayout* outerLayout = new QVBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0); outerLayout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(4); 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();
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);
outerLayout->addWidget(m_amountsRow);
hide(); hide();
} }
@@ -66,39 +93,40 @@ void RecipeLineRow::setLine(const Spec& spec)
void RecipeLineRow::rebuild(const Spec& spec) void RecipeLineRow::rebuild(const Spec& spec)
{ {
while (QLayoutItem* item = m_layout->takeAt(0)) clearRow(m_headerLayout);
{ clearRow(m_amountsLayout);
if (item->widget())
{
item->widget()->deleteLater();
}
delete item;
}
// The building the recipe runs in, where the line has to distinguish one producer // First line: which building runs the recipe and which recipe it is, where the line
// from another (REQ-UI-ITEM-TOOLTIP). A building with no chip file leaves the icon // has to distinguish one producer from another (REQ-UI-ITEM-TOOLTIP). A building with
// off, as everywhere else (REQ-UI-BUILD-ICON). // no chip file leaves the icon off, as everywhere else (REQ-UI-BUILD-ICON).
if (spec.building.has_value() && m_buildingIcons != nullptr) if (spec.building.has_value() && m_buildingIcons != nullptr)
{ {
const QPixmap chip = const QPixmap chip =
m_buildingIcons->getChip(buildingTypeId(*spec.building), kIconSizePx); m_buildingIcons->getChip(buildingTypeId(*spec.building), kIconSizePx);
if (!chip.isNull()) if (!chip.isNull())
{ {
QLabel* chipLabel = new QLabel(this); QLabel* chipLabel = new QLabel(m_headerRow);
chipLabel->setPixmap(chip); chipLabel->setPixmap(chip);
addAndShow(m_layout, chipLabel); addAndShow(m_headerLayout, chipLabel);
} }
} }
if (!spec.name.isEmpty()) if (!spec.name.isEmpty())
{ {
addAndShow(m_layout, new QLabel(spec.name, this)); addAndShow(m_headerLayout, new QLabel(spec.name, m_headerRow));
} }
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); addAmounts(spec.inputs);
if (!spec.inputs.empty() && !spec.outputs.empty()) if (!spec.inputs.empty() && !spec.outputs.empty())
{ {
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
addAndShow(m_layout, new QLabel(QString(rightArrow), this)); addAndShow(m_amountsLayout, new QLabel(QString(rightArrow), m_amountsRow));
} }
addAmounts(spec.outputs); addAmounts(spec.outputs);
@@ -108,10 +136,10 @@ void RecipeLineRow::rebuild(const Spec& spec)
const QString time = spec.durationIsAddition const QString time = spec.durationIsAddition
? tr("+%1 s").arg(*spec.durationSeconds, 0, 'f', 1) ? tr("+%1 s").arg(*spec.durationSeconds, 0, 'f', 1)
: tr("%1 s").arg(*spec.durationSeconds, 0, 'f', 1); : tr("%1 s").arg(*spec.durationSeconds, 0, 'f', 1);
addAndShow(m_layout, new QLabel( addAndShow(m_amountsLayout, new QLabel(
QStringLiteral("%1 %2").arg(middleDot).arg(time), this)); QStringLiteral("%1 %2").arg(middleDot).arg(time), m_amountsRow));
} }
m_layout->addStretch(1); m_amountsLayout->addStretch(1);
} }
void RecipeLineRow::addAmounts(const std::vector<Amount>& amounts) void RecipeLineRow::addAmounts(const std::vector<Amount>& amounts)
@@ -126,15 +154,16 @@ void RecipeLineRow::addAmounts(const std::vector<Amount>& amounts)
: QPixmap(); : QPixmap();
if (!icon.isNull()) if (!icon.isNull())
{ {
QLabel* iconLabel = new QLabel(this); QLabel* iconLabel = new QLabel(m_amountsRow);
iconLabel->setPixmap(icon); iconLabel->setPixmap(icon);
addAndShow(m_layout, iconLabel); addAndShow(m_amountsLayout, iconLabel);
} }
else else
{ {
addAndShow(m_layout, addAndShow(m_amountsLayout,
new QLabel(QString::fromStdString(entry.itemId), this)); new QLabel(QString::fromStdString(entry.itemId), m_amountsRow));
} }
addAndShow(m_layout, new QLabel(QString::number(entry.amount), this)); addAndShow(m_amountsLayout,
new QLabel(QString::number(entry.amount), m_amountsRow));
} }
} }

View File

@@ -13,9 +13,10 @@ class BuildingIconCache;
class ItemIconCache; class ItemIconCache;
class QHBoxLayout; class QHBoxLayout;
// One recipe drawn on a line: what it consumes, an arrow, what it produces, and how // One recipe drawn: what it consumes, an arrow, what it produces, and how long a cycle
// long a cycle takes (REQ-UI-RECIPE-SUMMARY). Every place the UI states what something // takes (REQ-UI-RECIPE-SUMMARY), under a header naming the building that runs it and the
// makes draws this same line, so a recipe reads alike wherever it is shown: // recipe itself where the recipe has to identify itself. Every place the UI states what
// something makes draws this same line, so a recipe reads alike wherever it is shown:
// //
// * beneath the selection button, for the recipe a building is running // * beneath the selection button, for the recipe a building is running
// (REQ-UI-RECIPE-SUMMARY); // (REQ-UI-RECIPE-SUMMARY);
@@ -48,12 +49,13 @@ public:
// lines. // lines.
struct Spec struct Spec
{ {
// Drawn as a leading chip when the line has to say which building runs the // The chip of the building running the recipe, on the header line, where the
// recipe -- an item may have several producers (REQ-UI-ITEM-TOOLTIP). Unset // recipe has to say which building that is -- an item may have several producers
// wherever the surrounding widget already establishes the building. // (REQ-UI-ITEM-TOOLTIP). Unset wherever the surrounding widget already
// establishes the building, and the header line is then left out entirely.
std::optional<BuildingType> building; std::optional<BuildingType> building;
// Leading recipe name, for the same reason. Empty where the name is the // The recipe's name, beside the building chip on the header line. Empty where the
// caption above the line instead. // name is the caption of the widget around this one instead.
QString name; QString name;
std::vector<Amount> inputs; std::vector<Amount> inputs;
// Empty for a line that produces no item of its own: a ship schematic, or a // Empty for a line that produces no item of its own: a ship schematic, or a
@@ -91,7 +93,13 @@ private:
ItemIconCache* m_itemIcons; ItemIconCache* m_itemIcons;
BuildingIconCache* m_buildingIcons; BuildingIconCache* m_buildingIcons;
QHBoxLayout* m_layout; // The two lines, each a row widget of its own so a rebuild only has to empty their
// layouts -- no nested layout to take apart. The header is hidden where the spec
// names neither a building nor a recipe.
QWidget* m_headerRow;
QHBoxLayout* m_headerLayout;
QWidget* m_amountsRow;
QHBoxLayout* m_amountsLayout;
// What the row currently shows, so a refresh at tick rate rebuilds it only when the // What the row currently shows, so a refresh at tick rate rebuilds it only when the
// recipe actually changed. // recipe actually changed.
Spec m_spec; Spec m_spec;

View File

@@ -80,7 +80,12 @@ SchematicChoiceDialog::SchematicChoiceDialog(
QWidget* card = new QWidget(this); QWidget* card = new QWidget(this);
QVBoxLayout* cardLayout = new QVBoxLayout(card); QVBoxLayout* cardLayout = new QVBoxLayout(card);
card->setStyleSheet("QWidget { border: 1px solid gray; padding: 8px; }"); // Scoped to the card by object name. An unscoped "QWidget" selector cascades to
// every descendant, which boxed each label and each icon of the recipe lines
// inside it as well.
card->setObjectName(QStringLiteral("schematicChoiceCard"));
card->setStyleSheet(QStringLiteral(
"QWidget#schematicChoiceCard { border: 1px solid gray; padding: 8px; }"));
QLabel* nameLabel = new QLabel(QString::fromStdString(option.displayName), card); QLabel* nameLabel = new QLabel(QString::fromStdString(option.displayName), card);
QFont nameFont = nameLabel->font(); QFont nameFont = nameLabel->font();