draw recipes wherever the UI states what something makes

Implements the requirements of 0bf24a7. Recipes were explained in prose:
the recipe dialog was a grid of icon-only buttons with a text tooltip
listing name, inputs, time and output, the unlock-choice dialog listed
recipe names with the same tooltip, an item chip said nothing about where
its item came from, and a module button never showed its cost.

RecipeLineRow is the one widget that draws a recipe -- optional building
chip and name, inputs, arrow, outputs, time -- and every place that states
what something makes now goes through it: the panel's recipe summary (which
it replaces RecipeSummaryRow for), the selection dialog's option buttons,
the item production tooltip, the unlock-choice dialog's recipe lines, the
module buttons' cost lines, and the layout dialog's build cost. Its arrow
is drawn only where there are outputs, so a shipyard summary no longer
points at nothing.

ItemProducers answers where an item comes from, filtering by unlock state
per building type, and ItemTooltip draws it as a window of its own -- Qt's
tooltips are text, this one has item squares and building chips in it.
ItemChip pops it after a hover delay.

The selection dialog becomes one scrolling vertical column of buttons, each
showing its name over its recipe line, and no dialog carries a tooltip any
more. RecipeTooltip and recipes.toml's unused "icon" field go with the
behavior they served.

Two requirement amendments the implementation forced, both in this commit:
the option column scrolls when taller than the window allows, and a Smelter
or Reprocessing recipe is listed once its building is unlocked, those
recipes carrying no unlock of their own. The layout dialog's build cost
also moved out of the shared ShipStatsPanel, which the balancing tool
compiles directly and is deliberately kept free of the icon caches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-11 21:20:40 +02:00
parent 0bf24a7c62
commit b7b5a92dbb
36 changed files with 934 additions and 442 deletions

View File

@@ -5,11 +5,15 @@
#include <functional>
#include "DisplayName.h"
#include "ProductionRules.h"
#include "RecipeLineRow.h"
#include "SectionBox.h"
#include <QGridLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QKeyEvent>
#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QPushButton>
@@ -22,6 +26,18 @@
namespace
{
std::vector<RecipeLineRow::Amount> toAmounts(
const std::vector<RecipeIngredient>& ingredients)
{
std::vector<RecipeLineRow::Amount> amounts;
amounts.reserve(ingredients.size());
for (const RecipeIngredient& ingredient : ingredients)
{
amounts.push_back(RecipeLineRow::Amount{ ingredient.item, ingredient.amount });
}
return amounts;
}
const int kCellSize = 32;
// Empty ship tiles pulse between this grey and white at 1 Hz while a module is
@@ -401,9 +417,11 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
std::vector<ShipLayoutBlueprint>& allBlueprints,
std::set<std::string> unlockedModuleIds,
bool debugDraw,
ItemIconCache* itemIcons,
QWidget* parent)
: QDialog(parent)
, m_config(config)
, m_itemIcons(itemIcons)
, m_shipId(shipId)
, m_unlockedModuleIds(std::move(unlockedModuleIds))
, m_rows(0)
@@ -463,10 +481,22 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
// Middle: three-column area (stats | module buttons | blueprints).
QHBoxLayout* columnsLayout = new QHBoxLayout();
// Left column: ship stats panel.
// Left column: ship stats panel, and beneath it what the layout being configured
// would cost to build (REQ-MOD-UI-DIALOG). The cost sits outside the stats panel
// because that widget is shared with the live ship card and the balancing tool,
// neither of which costs anything -- and the balancing tool is deliberately built
// without the item icons this line draws with.
QVBoxLayout* leftLayout = new QVBoxLayout();
m_statsPanel = new ShipStatsPanel(config, this);
m_statsPanel->setDebugDrawEnabled(m_debugDraw);
columnsLayout->addWidget(m_statsPanel);
leftLayout->addWidget(m_statsPanel);
m_buildCostSection = new SectionBox(tr("Build cost"), this);
m_buildCostLine = new RecipeLineRow(m_itemIcons, nullptr, m_buildCostSection);
m_buildCostSection->getContentLayout()->addWidget(m_buildCostLine);
leftLayout->addWidget(m_buildCostSection);
leftLayout->addStretch(1);
columnsLayout->addLayout(leftLayout);
// Center column: module selection buttons.
QVBoxLayout* centerLayout = new QVBoxLayout();
@@ -487,11 +517,34 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
m_moduleButtons.push_back(nullptr);
continue;
}
const QString label = QString::fromStdString(toDisplayName(def.id))
+ "\n" + QString::fromStdString(def.glyph);
QPushButton* btn = new QPushButton(label, this);
// The module's name and glyph over what it costs -- its materials and the
// production time it adds (REQ-MOD-UI-DIALOG). Both children are transparent to
// the mouse so a click anywhere on the face still reaches the button.
QPushButton* btn = new QPushButton(this);
btn->setCheckable(true);
btn->setFixedHeight(48);
QVBoxLayout* face = new QVBoxLayout(btn);
face->setContentsMargins(8, 4, 8, 4);
face->setSpacing(1);
QLabel* nameLabel = new QLabel(
QString::fromStdString(toDisplayName(def.id))
+ " " + QString::fromStdString(def.glyph), btn);
nameLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
face->addWidget(nameLabel);
RecipeLineRow::Spec cost;
cost.inputs = toAmounts(def.materials);
cost.durationSeconds = def.productionTimeSeconds;
cost.durationIsAddition = true;
RecipeLineRow* costLine = new RecipeLineRow(m_itemIcons, nullptr, btn);
costLine->setAttribute(Qt::WA_TransparentForMouseEvents, true);
face->addWidget(costLine);
costLine->setLine(cost);
// The config tooltip stays: it says what the module does, which the cost line
// does not (REQ-MOD-UI-MODULE-TOOLTIP).
if (def.tooltip)
{
btn->setToolTip(QString::fromStdString(*def.tooltip));
@@ -731,6 +784,29 @@ void ShipLayoutDialog::updateGridWidget()
void ShipLayoutDialog::updateStats()
{
m_statsPanel->refresh(m_shipId, m_placedModules);
// What the layout as it currently stands would cost to build, so the price follows
// every placement and removal (REQ-MOD-UI-DIALOG). The sums come from the
// simulation's own rules rather than being added up a second time here
// (REQ-MOD-MATERIALS, REQ-MOD-PRODUCTION-TIME).
ShipLayoutConfig layout;
layout.placedModules = m_placedModules;
const std::map<std::string, int> materials =
computeShipyardRequiredMaterials(*m_config, m_shipId, layout);
RecipeLineRow::Spec cost;
cost.inputs.reserve(materials.size());
for (const std::pair<const std::string, int>& entry : materials)
{
cost.inputs.push_back(RecipeLineRow::Amount{ entry.first, entry.second });
}
cost.durationSeconds =
computeShipyardProductionTimeSeconds(*m_config, m_shipId, layout);
m_buildCostLine->setLine(cost);
// A schematic costing no materials at all has no line to draw, and the section then
// stays out of the way.
m_buildCostSection->setVisible(!cost.inputs.empty());
}
bool ShipLayoutDialog::canPlaceModule(const ModuleDef& def, QPoint position,