show recipes visually instead of describing them in text

This commit is contained in:
2026-08-11 21:40:36 +02:00
parent 3274df91d4
commit f255224ccd
38 changed files with 1023 additions and 452 deletions

View File

@@ -5,11 +5,16 @@
#include <functional>
#include "DisplayName.h"
#include "OptionButton.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 +27,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 +418,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 +482,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 +518,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 over what it costs -- its materials and the production time
// it adds (REQ-MOD-UI-DIALOG). The glyph is how a module is identified on the
// layout grid, not here. Both children are transparent to the mouse so a click
// anywhere on the face still reaches the button.
OptionButton* btn = new OptionButton(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)), 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 +785,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,