split the selection panel into one card per kind of selection
The panel rendered every selection out of a single pool of member widgets,
hidden and shown per branch, so each build path had to remember to hide the
other branches' widgets. That coupling produced the two defects fixed in
668ce0f, and the per-type detail the requirements now ask for would only add
more of it.
The pool is gone. SelectionPanel keeps the category arbitration, the float and
the hide-when-empty behaviour, and hosts exactly one SelectionContent at a
time; SelectionContentFactory picks which one from the selection alone. Each
card is one row of the catalog in REQ-UI-SELECTION-CONTENT and owns only its
own widgets.
The card structure (REQ-UI-SELECTION-CARD) lives in the base class: a header
with an identity symbol, a name and one right slot, then a configuration group
and a runtime group. A construction site keeps its configuration and has its
whole runtime group replaced by the construction progress, decided once there
rather than in every card (REQ-BLD-SITE-CONFIG).
Also implemented here:
- REQ-UI-SELECTION-STATUS: the header status dot, taken from the simulation's
own getProductionStatus() so the panel and the world's status light cannot
disagree.
- REQ-UI-SELECTION-AGGREGATE: belt-subsystem tiles and debris-only selections
collapse into one card with a count instead of a count summary.
- REQ-UI-HQ-PANEL: the HQ shows the global block stock and its HP, neither of
which is a buffer.
- BuildingIconCache, extracted from BuildButtonBar's file-local chip loading so
the card headers and the build buttons rasterize the same SVGs once.
FieldSelectionPanel is deleted: ships, stations, debris and the field count
summary are four more cards in the same factory, so the two-panel arbitration
collapses into one decision.
The card parts are still today's labels and buttons; the item chips, bars,
recipe summary and stat rows follow.
Build clean, 541 tests pass, app runs with no Qt warnings. Visual check
pending.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
@@ -125,6 +125,7 @@ Three product targets plus tests:
|
|||||||
|
|
||||||
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
- `lib/` — simulation + config. Depends on Qt Core + Qt Gui, toml++, tinyexpr. No QtWidgets.
|
||||||
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selection panel, build button bar. Depends on `lib` and on Qt's OpenGL widgets module.
|
- `ui/` — QtWidgets + `QOpenGLWidget` code: header bar, game world view, selection panel, build button bar. Depends on `lib` and on Qt's OpenGL widgets module.
|
||||||
|
- `ui/selection/` — the selection panel's contents. `SelectionPanel` itself only arbitrates between the two selection categories, picks a card from the catalog (`SelectionContentFactory`), and hosts one at a time; each kind of selection has its own `SelectionContent` subclass assembled from shared parts (REQ-UI-SELECTION-CARD, REQ-UI-SELECTION-CONTENT).
|
||||||
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
- `app/` — thin `main()` that creates the simulation, the UI, and wires them together. Depends on `ui`.
|
||||||
- `tests/` — Catch2 tests. Links only against `lib`.
|
- `tests/` — Catch2 tests. Links only against `lib`.
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ unset(SRCS)
|
|||||||
|
|
||||||
set(HDRS)
|
set(HDRS)
|
||||||
set(SRCS)
|
set(SRCS)
|
||||||
|
set(UI_INCLUDE_PATH)
|
||||||
|
|
||||||
add_subdirectory(ui)
|
add_subdirectory(ui)
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ set_target_properties(${TARGET_UI_NAME} PROPERTIES
|
|||||||
)
|
)
|
||||||
target_include_directories(${TARGET_UI_NAME} PUBLIC
|
target_include_directories(${TARGET_UI_NAME} PUBLIC
|
||||||
"${TARGET_UI_INCLUDE_DIRS}"
|
"${TARGET_UI_INCLUDE_DIRS}"
|
||||||
|
"${UI_INCLUDE_PATH}"
|
||||||
"${TARGET_LIB_INCLUDE_DIRS}"
|
"${TARGET_LIB_INCLUDE_DIRS}"
|
||||||
"${LIB_INCLUDE_PATH}"
|
"${LIB_INCLUDE_PATH}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QFile>
|
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QFontMetrics>
|
#include <QFontMetrics>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
@@ -15,12 +13,11 @@
|
|||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QSignalMapper>
|
#include <QSignalMapper>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QSvgRenderer>
|
|
||||||
|
|
||||||
|
#include "BuildingIconCache.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "BuildingTypeSelectedEvent.h"
|
#include "BuildingTypeSelectedEvent.h"
|
||||||
#include "DeconstructModeToggleRequestedEvent.h"
|
#include "DeconstructModeToggleRequestedEvent.h"
|
||||||
@@ -54,45 +51,15 @@ namespace
|
|||||||
// Gap between the chip icon and the cost line on a button face.
|
// Gap between the chip icon and the cost line on a button face.
|
||||||
const int kFaceGapPx = 2;
|
const int kFaceGapPx = 2;
|
||||||
|
|
||||||
// Rasterizes a chip SVG straight at its on-screen size times the device pixel
|
// Normal and grey-background chip pixmaps for one icon name. Empty pixmaps if the
|
||||||
// ratio, so it stays crisp without a downscale step.
|
// file cannot be read; the caller then falls back to a name caption
|
||||||
QPixmap renderChip(const QByteArray& svg)
|
|
||||||
{
|
|
||||||
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
|
||||||
QSvgRenderer renderer(svg);
|
|
||||||
QPixmap pixmap(static_cast<int>(kIconSize.width() * dpr),
|
|
||||||
static_cast<int>(kIconSize.height() * dpr));
|
|
||||||
pixmap.setDevicePixelRatio(dpr);
|
|
||||||
pixmap.fill(Qt::transparent);
|
|
||||||
QPainter painter(&pixmap);
|
|
||||||
renderer.render(&painter);
|
|
||||||
return pixmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal and grey-background chip pixmaps for a "<id>.svg" file. Empty pixmaps if
|
|
||||||
// the file cannot be read; the caller then falls back to a name caption
|
|
||||||
// (REQ-UI-BUILD-ICON).
|
// (REQ-UI-BUILD-ICON).
|
||||||
struct ChipPixmaps { QPixmap normal; QPixmap grey; };
|
struct ChipPixmaps { QPixmap normal; QPixmap grey; };
|
||||||
ChipPixmaps loadChipPixmaps(const QString& path)
|
ChipPixmaps loadChipPixmaps(BuildingIconCache& icons, const std::string& iconName)
|
||||||
{
|
{
|
||||||
QFile file(path);
|
|
||||||
if (!file.open(QIODevice::ReadOnly)) { return {}; }
|
|
||||||
const QByteArray svg = file.readAll();
|
|
||||||
|
|
||||||
ChipPixmaps result;
|
ChipPixmaps result;
|
||||||
result.normal = renderChip(svg);
|
result.normal = icons.getChip(iconName, kIconSize.width());
|
||||||
|
result.grey = icons.getGreyChip(iconName, kIconSize.width());
|
||||||
// Recolor only the chip background: the first "#rrggbb" fill in the file is the
|
|
||||||
// rounded background rect; the white glyph uses fill="none" and is left alone.
|
|
||||||
QString greyed = QString::fromUtf8(svg);
|
|
||||||
static const QRegularExpression fillPattern(QStringLiteral("fill=\"#[0-9a-fA-F]{6}\""));
|
|
||||||
const QRegularExpressionMatch match = fillPattern.match(greyed);
|
|
||||||
if (match.hasMatch())
|
|
||||||
{
|
|
||||||
greyed.replace(match.capturedStart(), match.capturedLength(),
|
|
||||||
QStringLiteral("fill=\"#5f636e\""));
|
|
||||||
}
|
|
||||||
result.grey = renderChip(greyed.toUtf8());
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,12 +148,12 @@ namespace
|
|||||||
|
|
||||||
|
|
||||||
BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
|
BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
|
||||||
const std::string& iconDir,
|
BuildingIconCache* buildingIcons,
|
||||||
ItemIconCache* itemIcons, QWidget* parent)
|
ItemIconCache* itemIcons, QWidget* parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_sim(sim)
|
, m_sim(sim)
|
||||||
, m_config(config)
|
, m_config(config)
|
||||||
, m_iconDir(iconDir)
|
, m_buildingIcons(buildingIcons)
|
||||||
, m_itemIcons(itemIcons)
|
, m_itemIcons(itemIcons)
|
||||||
{
|
{
|
||||||
// The bar floats over the rendered world rather than sitting in a panel, so it
|
// The bar floats over the rendered world rather than sitting in a panel, so it
|
||||||
@@ -231,13 +198,12 @@ BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
|
|||||||
const QString name = (def.type == BuildingType::TunnelEntry)
|
const QString name = (def.type == BuildingType::TunnelEntry)
|
||||||
? tr("Tunnel")
|
? tr("Tunnel")
|
||||||
: QString::fromStdString(toDisplayName(def.id));
|
: QString::fromStdString(toDisplayName(def.id));
|
||||||
|
|
||||||
// Icon file name matches the building id (REQ-UI-BUILD-ICON); Tunnel Entry's
|
// Icon file name matches the building id (REQ-UI-BUILD-ICON); Tunnel Entry's
|
||||||
// "tunnel_entry.svg" serves the shared Tunnel button.
|
// "tunnel_entry.svg" serves the shared Tunnel button.
|
||||||
const QString iconPath = QString::fromStdString(m_iconDir) + "/"
|
|
||||||
+ QString::fromStdString(def.id) + ".svg";
|
|
||||||
|
|
||||||
const ButtonFace face = buildButtonFace(
|
const ButtonFace face = buildButtonFace(
|
||||||
loadChipPixmaps(iconPath), InputMapper::getBuildHotkeyLabel(def.type), name,
|
loadChipPixmaps(*m_buildingIcons, def.id),
|
||||||
|
InputMapper::getBuildHotkeyLabel(def.type), name,
|
||||||
QString::number(def.cost), blockIcon, font(), palette());
|
QString::number(def.cost), blockIcon, font(), palette());
|
||||||
|
|
||||||
QPushButton* btn = new QPushButton(this);
|
QPushButton* btn = new QPushButton(this);
|
||||||
@@ -268,7 +234,7 @@ BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
|
|||||||
// Having no cost, it shows its name where the building buttons show theirs
|
// Having no cost, it shows its name where the building buttons show theirs
|
||||||
// (REQ-UI-DECONSTRUCT-BUTTON), and its Q toggle as the badge (REQ-UI-HOTKEYS).
|
// (REQ-UI-DECONSTRUCT-BUTTON), and its Q toggle as the badge (REQ-UI-HOTKEYS).
|
||||||
const ButtonFace deconstructFace = buildButtonFace(
|
const ButtonFace deconstructFace = buildButtonFace(
|
||||||
loadChipPixmaps(QString::fromStdString(m_iconDir) + "/deconstruct.svg"),
|
loadChipPixmaps(*m_buildingIcons, "deconstruct"),
|
||||||
QStringLiteral("Q"), tr("Deconstruct"), tr("Deconstruct"), QPixmap(),
|
QStringLiteral("Q"), tr("Deconstruct"), tr("Deconstruct"), QPixmap(),
|
||||||
font(), palette());
|
font(), palette());
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class Simulation;
|
class Simulation;
|
||||||
|
class BuildingIconCache;
|
||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
|
|
||||||
// The build menu: one horizontal row of build buttons floating over the game world
|
// The build menu: one horizontal row of build buttons floating over the game world
|
||||||
@@ -36,13 +37,11 @@ class BuildButtonBar : public QWidget,
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// iconDir is the directory holding the per-building "<id>.svg" chip icons
|
// buildingIcons supplies each button's chip icon (REQ-UI-BUILD-ICON) and itemIcons
|
||||||
// (REQ-UI-BUILD-ICON), read from disk at runtime like the config files.
|
// the building_block icon shown in each button's cost (REQ-UI-BUILD-COST). Both are
|
||||||
// itemIcons is the window-wide per-item icon cache and supplies the
|
// window-wide caches; neither is owned, and both must outlive this widget.
|
||||||
// building_block icon shown in each button's cost (REQ-UI-BUILD-COST). Not
|
|
||||||
// owned; must outlive this widget.
|
|
||||||
BuildButtonBar(Simulation* sim, const GameConfig* config,
|
BuildButtonBar(Simulation* sim, const GameConfig* config,
|
||||||
const std::string& iconDir, ItemIconCache* itemIcons,
|
BuildingIconCache* buildingIcons, ItemIconCache* itemIcons,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~BuildButtonBar() override;
|
~BuildButtonBar() override;
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
Simulation* m_sim;
|
Simulation* m_sim;
|
||||||
const GameConfig* m_config;
|
const GameConfig* m_config;
|
||||||
std::string m_iconDir;
|
BuildingIconCache* m_buildingIcons; // Not owned; lives in MainWindow.
|
||||||
ItemIconCache* m_itemIcons; // Not owned; lives in MainWindow.
|
ItemIconCache* m_itemIcons; // Not owned; lives in MainWindow.
|
||||||
std::vector<BuildingType> m_types;
|
std::vector<BuildingType> m_types;
|
||||||
std::vector<QPushButton*> m_buttons;
|
std::vector<QPushButton*> m_buttons;
|
||||||
|
|||||||
120
src/ui/BuildingIconCache.cpp
Normal file
120
src/ui/BuildingIconCache.cpp
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
#include "BuildingIconCache.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSvgRenderer>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Chip background color for a build button the player cannot afford
|
||||||
|
// (REQ-UI-BUILD-DISABLED). Part of the icon rather than of any widget's palette, so it
|
||||||
|
// lives with the recoloring step.
|
||||||
|
const char* const kGreyFill = "fill=\"#5f636e\"";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
BuildingIconCache::BuildingIconCache(const QString& iconDir)
|
||||||
|
: m_iconDir(iconDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray& BuildingIconCache::getSvg(const std::string& iconName)
|
||||||
|
{
|
||||||
|
const std::map<std::string, QByteArray>::const_iterator cached =
|
||||||
|
m_svgByName.find(iconName);
|
||||||
|
if (cached != m_svgByName.end())
|
||||||
|
{
|
||||||
|
return cached->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An absent or unreadable file caches an empty byte array so it is not retried;
|
||||||
|
// a missing icon is not an error (REQ-UI-BUILD-ICON).
|
||||||
|
QByteArray svg;
|
||||||
|
QFile file(m_iconDir + "/" + QString::fromStdString(iconName) + ".svg");
|
||||||
|
if (file.open(QIODevice::ReadOnly))
|
||||||
|
{
|
||||||
|
svg = file.readAll();
|
||||||
|
}
|
||||||
|
return m_svgByName.emplace(iconName, std::move(svg)).first->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray& BuildingIconCache::getGreySvg(const std::string& iconName)
|
||||||
|
{
|
||||||
|
const std::map<std::string, QByteArray>::const_iterator cached =
|
||||||
|
m_greySvgByName.find(iconName);
|
||||||
|
if (cached != m_greySvgByName.end())
|
||||||
|
{
|
||||||
|
return cached->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recolor only the chip background: the first "#rrggbb" fill in the file is the
|
||||||
|
// rounded background rect; the white glyph uses fill="none" and is left alone.
|
||||||
|
const QByteArray& svg = getSvg(iconName);
|
||||||
|
QByteArray greyed = svg;
|
||||||
|
if (!svg.isEmpty())
|
||||||
|
{
|
||||||
|
QString text = QString::fromUtf8(svg);
|
||||||
|
static const QRegularExpression fillPattern(
|
||||||
|
QStringLiteral("fill=\"#[0-9a-fA-F]{6}\""));
|
||||||
|
const QRegularExpressionMatch match = fillPattern.match(text);
|
||||||
|
if (match.hasMatch())
|
||||||
|
{
|
||||||
|
text.replace(match.capturedStart(), match.capturedLength(),
|
||||||
|
QLatin1String(kGreyFill));
|
||||||
|
}
|
||||||
|
greyed = text.toUtf8();
|
||||||
|
}
|
||||||
|
return m_greySvgByName.emplace(iconName, std::move(greyed)).first->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuildingIconCache::hasIcon(const std::string& iconName)
|
||||||
|
{
|
||||||
|
return !getSvg(iconName).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap BuildingIconCache::getChip(const std::string& iconName, int sizePx)
|
||||||
|
{
|
||||||
|
return getPixmap(iconName, getSvg(iconName), sizePx);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap BuildingIconCache::getGreyChip(const std::string& iconName, int sizePx)
|
||||||
|
{
|
||||||
|
return getPixmap("grey:" + iconName, getGreySvg(iconName), sizePx);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap BuildingIconCache::getPixmap(const std::string& cacheKey,
|
||||||
|
const QByteArray& svg, int sizePx)
|
||||||
|
{
|
||||||
|
if (sizePx <= 0)
|
||||||
|
{
|
||||||
|
return QPixmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::pair<std::string, int> key(cacheKey, sizePx);
|
||||||
|
const std::map<std::pair<std::string, int>, QPixmap>::const_iterator cached =
|
||||||
|
m_pixmapCache.find(key);
|
||||||
|
if (cached != m_pixmapCache.end())
|
||||||
|
{
|
||||||
|
return cached->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap pixmap;
|
||||||
|
if (!svg.isEmpty())
|
||||||
|
{
|
||||||
|
// Rasterized straight at its on-screen size times the device pixel ratio, so it
|
||||||
|
// stays crisp without a downscale step.
|
||||||
|
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||||
|
QSvgRenderer renderer(svg);
|
||||||
|
pixmap = QPixmap(static_cast<int>(sizePx * dpr), static_cast<int>(sizePx * dpr));
|
||||||
|
pixmap.setDevicePixelRatio(dpr);
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
QPainter painter(&pixmap);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
renderer.render(&painter);
|
||||||
|
}
|
||||||
|
return m_pixmapCache.emplace(key, std::move(pixmap)).first->second;
|
||||||
|
}
|
||||||
55
src/ui/BuildingIconCache.h
Normal file
55
src/ui/BuildingIconCache.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// Rasterizes and caches the per-building chip SVGs (REQ-UI-BUILD-ICON). A chip is a
|
||||||
|
// rounded colored background bearing a white line glyph, loaded from a directory with
|
||||||
|
// one file per building named after the building's id (e.g. "belt.svg"). Shared by the
|
||||||
|
// build button bar and the selection panel's card header (REQ-UI-SELECTION-CARD) so the
|
||||||
|
// rasterization is not duplicated.
|
||||||
|
//
|
||||||
|
// A missing icon file is not an error: hasIcon() returns false for it and the caller
|
||||||
|
// falls back (a name caption on a build button, a bare title in the panel header).
|
||||||
|
class BuildingIconCache
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// iconDir is the directory holding the "<id>.svg" chip files (typically
|
||||||
|
// "<configDir>/../icons/buildings").
|
||||||
|
explicit BuildingIconCache(const QString& iconDir);
|
||||||
|
|
||||||
|
// True if a chip SVG file exists for the given icon name. Loads the file's bytes on
|
||||||
|
// first query and remembers the result (including absence) so repeated calls are
|
||||||
|
// cheap. The name is a building id for the building buttons, but need not be one --
|
||||||
|
// the Deconstruct button's "deconstruct" chip goes through the same path.
|
||||||
|
bool hasIcon(const std::string& iconName);
|
||||||
|
|
||||||
|
// The chip rasterized to a transparent sizePx*sizePx pixmap at the device pixel
|
||||||
|
// ratio, cached per (name, size). Returns a null pixmap when the file is missing.
|
||||||
|
QPixmap getChip(const std::string& iconName, int sizePx);
|
||||||
|
|
||||||
|
// As getChip(), but with the chip background recolored grey and the glyph left
|
||||||
|
// alone, for a build button the player cannot currently afford
|
||||||
|
// (REQ-UI-BUILD-DISABLED).
|
||||||
|
QPixmap getGreyChip(const std::string& iconName, int sizePx);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Raw SVG bytes for an icon name, loading and caching them on first access. An
|
||||||
|
// absent file caches an empty QByteArray so it is not retried.
|
||||||
|
const QByteArray& getSvg(const std::string& iconName);
|
||||||
|
// The same SVG with its background fill replaced by grey, cached alongside.
|
||||||
|
const QByteArray& getGreySvg(const std::string& iconName);
|
||||||
|
// Shared rasterize-and-cache step. cacheKey distinguishes the normal and grey
|
||||||
|
// variants of one icon name within the single pixmap cache.
|
||||||
|
QPixmap getPixmap(const std::string& cacheKey, const QByteArray& svg, int sizePx);
|
||||||
|
|
||||||
|
QString m_iconDir;
|
||||||
|
std::map<std::string, QByteArray> m_svgByName;
|
||||||
|
std::map<std::string, QByteArray> m_greySvgByName;
|
||||||
|
std::map<std::pair<std::string, int>, QPixmap> m_pixmapCache;
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
add_subdirectory(selection)
|
||||||
|
|
||||||
SET(HDRS
|
SET(HDRS
|
||||||
${HDRS}
|
${HDRS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/VisualsConfig.h
|
||||||
@@ -12,7 +14,6 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.h
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||||
@@ -22,10 +23,16 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.h
|
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(UI_INCLUDE_PATH
|
||||||
|
${UI_INCLUDE_PATH}
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
|
|
||||||
SET(SRCS
|
SET(SRCS
|
||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.cpp
|
||||||
@@ -38,7 +45,6 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.cpp
|
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
||||||
@@ -48,6 +54,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,403 +0,0 @@
|
|||||||
#include "FieldSelectionPanel.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <map>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <QFont>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QStringList>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
#include "DebrisSystem.h"
|
|
||||||
#include "DisplayName.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "FactionComponent.h"
|
|
||||||
#include "GameConfig.h"
|
|
||||||
#include "HealthComponent.h"
|
|
||||||
#include "ModuleOwnerComponent.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "ShipIdentityComponent.h"
|
|
||||||
#include "ShipStatsCalculator.h"
|
|
||||||
#include "ShipStatsPanel.h"
|
|
||||||
#include "Simulation.h"
|
|
||||||
#include "StationBodyComponent.h"
|
|
||||||
#include "ThreatCostCalculator.h"
|
|
||||||
#include "WeaponComponent.h"
|
|
||||||
|
|
||||||
|
|
||||||
FieldSelectionPanel::FieldSelectionPanel(Simulation* sim,
|
|
||||||
const GameConfig* config,
|
|
||||||
QWidget* parent)
|
|
||||||
: QWidget(parent)
|
|
||||||
, m_sim(sim)
|
|
||||||
, m_config(config)
|
|
||||||
{
|
|
||||||
// Zero margins and the same spacing as the enclosing SelectionPanel layout, so
|
|
||||||
// nesting the field widgets in this panel leaves their geometry unchanged.
|
|
||||||
m_layout = new QVBoxLayout(this);
|
|
||||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
m_layout->setSpacing(4);
|
|
||||||
m_layout->setAlignment(Qt::AlignTop);
|
|
||||||
|
|
||||||
m_entityTitleLabel = new QLabel(this);
|
|
||||||
QFont titleFont = m_entityTitleLabel->font();
|
|
||||||
titleFont.setBold(true);
|
|
||||||
m_entityTitleLabel->setFont(titleFont);
|
|
||||||
m_layout->addWidget(m_entityTitleLabel);
|
|
||||||
m_entityTitleLabel->hide();
|
|
||||||
|
|
||||||
m_entityStatsPanel = new ShipStatsPanel(config, this);
|
|
||||||
m_layout->addWidget(m_entityStatsPanel);
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
|
|
||||||
m_stationStatsLabel = new QLabel(this);
|
|
||||||
m_stationStatsLabel->setWordWrap(true);
|
|
||||||
m_layout->addWidget(m_stationStatsLabel);
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
|
|
||||||
m_entitySummaryLabel = new QLabel(this);
|
|
||||||
m_entitySummaryLabel->setWordWrap(true);
|
|
||||||
m_layout->addWidget(m_entitySummaryLabel);
|
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
|
|
||||||
m_scrapLabel = new QLabel(this);
|
|
||||||
m_layout->addWidget(m_scrapLabel);
|
|
||||||
m_scrapLabel->hide();
|
|
||||||
|
|
||||||
hide();
|
|
||||||
|
|
||||||
registerForEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
FieldSelectionPanel::~FieldSelectionPanel()
|
|
||||||
{
|
|
||||||
unregisterForEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::setSelectedEntities(const std::vector<entt::entity>& entities)
|
|
||||||
{
|
|
||||||
m_selectedEntities = entities;
|
|
||||||
rebuild();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::setSelectedDebris(const std::vector<entt::entity>& debris)
|
|
||||||
{
|
|
||||||
m_selectedDebris = debris;
|
|
||||||
rebuild();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::clearSelection()
|
|
||||||
{
|
|
||||||
m_selectedEntities.clear();
|
|
||||||
m_selectedDebris.clear();
|
|
||||||
rebuild();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool FieldSelectionPanel::hasSelection() const
|
|
||||||
{
|
|
||||||
return !m_selectedEntities.empty() || !m_selectedDebris.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::hideAllWidgets()
|
|
||||||
{
|
|
||||||
m_entityTitleLabel->hide();
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
m_scrapLabel->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::rebuild()
|
|
||||||
{
|
|
||||||
if (!hasSelection())
|
|
||||||
{
|
|
||||||
// Nothing in the field category: take no space, leaving the panel to whatever
|
|
||||||
// the building category shows (REQ-UI-SELECTION-CATEGORIES).
|
|
||||||
hideAllWidgets();
|
|
||||||
hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
show();
|
|
||||||
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
|
|
||||||
// A full single-object stats panel is shown only for a lone field object: one actor
|
|
||||||
// with no debris, or one piece of debris with no actors. As soon as the selection holds
|
|
||||||
// more than one object (multiple actors, multiple debris, or actors plus debris), the
|
|
||||||
// panel switches to the compact count summary (REQ-UI-FIELD-MULTI-SELECTION).
|
|
||||||
if (m_selectedEntities.size() == 1 && m_selectedDebris.empty())
|
|
||||||
{
|
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
m_scrapLabel->hide();
|
|
||||||
const entt::entity entity = m_selectedEntities.front();
|
|
||||||
if (admin.isValid(entity) && admin.hasAll<ShipIdentityComponent>(entity))
|
|
||||||
{
|
|
||||||
buildEntityShip(entity);
|
|
||||||
}
|
|
||||||
else if (admin.isValid(entity) && admin.hasAll<StationBodyComponent>(entity))
|
|
||||||
{
|
|
||||||
buildEntityStation(entity);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_entityTitleLabel->hide();
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_selectedEntities.empty() && m_selectedDebris.size() == 1)
|
|
||||||
{
|
|
||||||
// Single piece of debris: a "Debris" heading plus a "Scrap" stat row, styled like
|
|
||||||
// the ship/station stats panels (REQ-UI-DEBRIS-PANEL).
|
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
buildDebrisSingle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// More than one field object: a compact count summary. buildEntitySummary() appends the
|
|
||||||
// "Debris x N" and "Scrap x N" lines when debris is part of the selection.
|
|
||||||
m_entityTitleLabel->hide();
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
m_scrapLabel->hide();
|
|
||||||
buildEntitySummary();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::refreshDisplay()
|
|
||||||
{
|
|
||||||
if (!hasSelection()) { return; }
|
|
||||||
|
|
||||||
// Keep the live values current: the single-actor stats panel, the single-debris stats
|
|
||||||
// panel (whose Scrap row shrinks as it is collected), or the count summary (whose Scrap
|
|
||||||
// line shrinks likewise) — matching the layout chosen by rebuild()
|
|
||||||
// (REQ-UI-SHIP-STATS-PANEL, REQ-UI-DEBRIS-PANEL).
|
|
||||||
if (m_selectedEntities.size() == 1 && m_selectedDebris.empty())
|
|
||||||
{
|
|
||||||
refreshEntityStats();
|
|
||||||
}
|
|
||||||
else if (m_selectedEntities.empty() && m_selectedDebris.size() == 1)
|
|
||||||
{
|
|
||||||
buildDebrisSingle();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buildEntitySummary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::buildDebrisSingle()
|
|
||||||
{
|
|
||||||
// "Debris" heading + a single "Scrap" stat row for the piece's remaining amount,
|
|
||||||
// mirroring the single-actor stats panels (REQ-UI-DEBRIS-PANEL).
|
|
||||||
m_entityTitleLabel->setText(tr("Debris"));
|
|
||||||
m_entityTitleLabel->show();
|
|
||||||
m_scrapLabel->setText(tr("Scrap: %1").arg(selectedDebrisScrapTotal()));
|
|
||||||
m_scrapLabel->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::buildEntitySummary()
|
|
||||||
{
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
|
|
||||||
// Group actors by faction + kind + ship schematic, preserving first-seen order
|
|
||||||
// (REQ-UI-FIELD-MULTI-SELECTION).
|
|
||||||
std::vector<QString> keys;
|
|
||||||
std::map<QString, int> counts;
|
|
||||||
std::map<QString, QString> labels;
|
|
||||||
|
|
||||||
for (entt::entity entity : m_selectedEntities)
|
|
||||||
{
|
|
||||||
if (!admin.isValid(entity)) { continue; }
|
|
||||||
const bool isEnemy = admin.hasAll<FactionComponent>(entity)
|
|
||||||
&& admin.get<FactionComponent>(entity).isEnemy;
|
|
||||||
|
|
||||||
QString key;
|
|
||||||
QString label;
|
|
||||||
if (admin.hasAll<ShipIdentityComponent>(entity))
|
|
||||||
{
|
|
||||||
const std::string& id = admin.get<ShipIdentityComponent>(entity).schematicId;
|
|
||||||
const QString name = QString::fromStdString(toDisplayName(id));
|
|
||||||
key = (isEnemy ? QStringLiteral("ship:enemy:") : QStringLiteral("ship:player:"))
|
|
||||||
+ QString::fromStdString(id);
|
|
||||||
label = isEnemy ? tr("Enemy %1").arg(name) : name;
|
|
||||||
}
|
|
||||||
else if (admin.hasAll<StationBodyComponent>(entity))
|
|
||||||
{
|
|
||||||
key = isEnemy ? QStringLiteral("station:enemy") : QStringLiteral("station:player");
|
|
||||||
label = isEnemy ? tr("Enemy Defence Station") : tr("Player Defence Station");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counts.find(key) == counts.end())
|
|
||||||
{
|
|
||||||
keys.push_back(key);
|
|
||||||
labels[key] = label;
|
|
||||||
}
|
|
||||||
counts[key] += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One "<type> x <count>" line per group (matching the recipe tooltip and the building
|
|
||||||
// multi-selection). No total-count header, consistent with the building panel. When
|
|
||||||
// debris is part of the selection, a "Debris x <count>" line followed by a
|
|
||||||
// "Scrap x <total>" line are appended into the same label so the line spacing is
|
|
||||||
// uniform (REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-DEBRIS-PANEL).
|
|
||||||
QStringList lines;
|
|
||||||
for (const QString& key : keys)
|
|
||||||
{
|
|
||||||
lines << tr("%1 x %2").arg(labels[key]).arg(counts[key]);
|
|
||||||
}
|
|
||||||
if (!m_selectedDebris.empty())
|
|
||||||
{
|
|
||||||
lines << tr("Debris x %1").arg(static_cast<int>(m_selectedDebris.size()));
|
|
||||||
lines << scrapTotalText();
|
|
||||||
}
|
|
||||||
m_entitySummaryLabel->setText(lines.join('\n'));
|
|
||||||
m_entitySummaryLabel->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::buildEntityShip(entt::entity entity)
|
|
||||||
{
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
|
||||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
|
||||||
|
|
||||||
m_entityTitleLabel->setText(tr("Ship: %1")
|
|
||||||
.arg(QString::fromStdString(identity.schematicId)));
|
|
||||||
m_entityTitleLabel->show();
|
|
||||||
|
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
|
||||||
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
|
||||||
m_entityStatsPanel->setBehavior(
|
|
||||||
admin.get<SelectedBehaviorComponent>(entity).winner);
|
|
||||||
m_entityStatsPanel->setDebugDrawEnabled(m_debugDraw);
|
|
||||||
|
|
||||||
const ShipDef* schematicDef =
|
|
||||||
m_config->ships.findShipDef(identity.schematicId);
|
|
||||||
if (schematicDef)
|
|
||||||
{
|
|
||||||
const double threat = calculateShipThreatCost(
|
|
||||||
m_config->threatCosts, *m_config, schematicDef->id,
|
|
||||||
schematicDef->defaultModules);
|
|
||||||
m_entityStatsPanel->setThreatCost(threat);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_entityStatsPanel->show();
|
|
||||||
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::buildEntityStation(entt::entity entity)
|
|
||||||
{
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
|
||||||
|
|
||||||
const bool isEnemy = admin.hasAll<FactionComponent>(entity)
|
|
||||||
&& admin.get<FactionComponent>(entity).isEnemy;
|
|
||||||
m_entityTitleLabel->setText(isEnemy
|
|
||||||
? tr("Enemy Defence Station")
|
|
||||||
: tr("Player Defence Station"));
|
|
||||||
m_entityTitleLabel->show();
|
|
||||||
|
|
||||||
float totalDps = 0.0f;
|
|
||||||
float maxRange = 0.0f;
|
|
||||||
bool hasWeapons = false;
|
|
||||||
|
|
||||||
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
|
||||||
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w)
|
|
||||||
{
|
|
||||||
if (owner.owner != entity) { return; }
|
|
||||||
hasWeapons = true;
|
|
||||||
totalDps += w.damage * w.fireRateHz;
|
|
||||||
if (w.range_tiles > maxRange) { maxRange = w.range_tiles; }
|
|
||||||
});
|
|
||||||
|
|
||||||
QString statsText = tr("HP: %1 / %2")
|
|
||||||
.arg(static_cast<int>(health.hp + 0.5f))
|
|
||||||
.arg(static_cast<int>(health.maxHp + 0.5f));
|
|
||||||
|
|
||||||
if (hasWeapons)
|
|
||||||
{
|
|
||||||
statsText += tr("\nDPS: %1").arg(QString::number(static_cast<double>(totalDps), 'f', 1));
|
|
||||||
statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast<double>(maxRange), 'f', 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_stationStatsLabel->setText(statsText);
|
|
||||||
m_stationStatsLabel->show();
|
|
||||||
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::refreshEntityStats()
|
|
||||||
{
|
|
||||||
// Only the single-actor stats panel needs a live refresh; the multi-actor summary is
|
|
||||||
// static counts, and GameWorldView prunes dead/despawned actors and re-emits the
|
|
||||||
// selection (REQ-UI-ENTITY-CLICK-SELECT), so the panel does not mutate it here.
|
|
||||||
if (m_selectedEntities.size() != 1) { return; }
|
|
||||||
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
const entt::entity entity = m_selectedEntities.front();
|
|
||||||
|
|
||||||
if (!admin.isValid(entity) || !admin.hasAll<HealthComponent>(entity)) { return; }
|
|
||||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
|
||||||
if (health.hp <= 0.0f) { return; }
|
|
||||||
|
|
||||||
if (admin.hasAll<ShipIdentityComponent>(entity))
|
|
||||||
{
|
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
|
||||||
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
|
||||||
m_entityStatsPanel->setBehavior(
|
|
||||||
admin.get<SelectedBehaviorComponent>(entity).winner);
|
|
||||||
}
|
|
||||||
else if (admin.hasAll<StationBodyComponent>(entity))
|
|
||||||
{
|
|
||||||
buildEntityStation(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int FieldSelectionPanel::selectedDebrisScrapTotal() const
|
|
||||||
{
|
|
||||||
// Sum the remaining scrap across the still-living selected debris (REQ-UI-DEBRIS-PANEL).
|
|
||||||
int total = 0;
|
|
||||||
for (const DebrisInfo& info : getAllDebrisInfo(m_sim->getAdmin()))
|
|
||||||
{
|
|
||||||
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
|
|
||||||
!= m_selectedDebris.end())
|
|
||||||
{
|
|
||||||
total += info.amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString FieldSelectionPanel::scrapTotalText() const
|
|
||||||
{
|
|
||||||
return tr("Scrap x %1").arg(selectedDebrisScrapTotal());
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
|
|
||||||
{
|
|
||||||
refreshDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::handleEvent(
|
|
||||||
std::shared_ptr<const PlayerCommandsAppliedEvent> /*event*/)
|
|
||||||
{
|
|
||||||
// Player commands are applied by a queued drain, not synchronously. When the game is
|
|
||||||
// paused no tick advances, so TickAdvancedEvent never fires; refresh here too.
|
|
||||||
refreshDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
void FieldSelectionPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)
|
|
||||||
{
|
|
||||||
m_debugDraw = event->active;
|
|
||||||
m_entityStatsPanel->setDebugDrawEnabled(event->active);
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QString>
|
|
||||||
#include <QWidget>
|
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
|
||||||
|
|
||||||
#include "DebugDrawToggledEvent.h"
|
|
||||||
#include "EventHandler.h"
|
|
||||||
#include "PlayerCommandsAppliedEvent.h"
|
|
||||||
#include "TickAdvancedEvent.h"
|
|
||||||
|
|
||||||
struct GameConfig;
|
|
||||||
class Simulation;
|
|
||||||
class ShipStatsPanel;
|
|
||||||
class QLabel;
|
|
||||||
class QVBoxLayout;
|
|
||||||
|
|
||||||
// Renders the "field" selection category — ships, defence stations and debris — as either
|
|
||||||
// a single-object stats panel (ship, station, or debris) or a compact multi-object count
|
|
||||||
// summary (REQ-UI-SELECTION-CATEGORIES, REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-DEBRIS-PANEL).
|
|
||||||
//
|
|
||||||
// The panel owns its own selection state and its own widgets, and nothing else. Which of
|
|
||||||
// the two selection categories owns the selection panel is arbitrated by the parent
|
|
||||||
// SelectionPanel: it feeds this panel through setSelectedEntities() /
|
|
||||||
// setSelectedDebris() / clearSelection() and asks it via hasSelection(). This panel hides
|
|
||||||
// itself whenever its selection is empty, so an inactive field category takes no space.
|
|
||||||
class FieldSelectionPanel : public QWidget,
|
|
||||||
public CombinedEventHandler<TickAdvancedEvent,
|
|
||||||
PlayerCommandsAppliedEvent,
|
|
||||||
DebugDrawToggledEvent>
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
FieldSelectionPanel(Simulation* sim, const GameConfig* config,
|
|
||||||
QWidget* parent = nullptr);
|
|
||||||
~FieldSelectionPanel() override;
|
|
||||||
|
|
||||||
// Replaces the selected actors (ships and defence stations); debris is left alone,
|
|
||||||
// the two coexist within the field category (REQ-UI-SELECTION-CATEGORIES).
|
|
||||||
void setSelectedEntities(const std::vector<entt::entity>& entities);
|
|
||||||
// Replaces the selected debris; the selected actors are left alone.
|
|
||||||
void setSelectedDebris(const std::vector<entt::entity>& debris);
|
|
||||||
// Drops the whole field selection — used when the building category takes over.
|
|
||||||
void clearSelection();
|
|
||||||
// True while the field category has anything selected, i.e. while this panel owns
|
|
||||||
// the selection panel's content.
|
|
||||||
bool hasSelection() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
|
||||||
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
|
|
||||||
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
|
||||||
|
|
||||||
// Picks the layout for the current selection and shows/hides this panel accordingly.
|
|
||||||
void rebuild();
|
|
||||||
// Keeps the live values of the layout chosen by rebuild() current.
|
|
||||||
void refreshDisplay();
|
|
||||||
void buildEntityShip(entt::entity entity);
|
|
||||||
void buildEntityStation(entt::entity entity);
|
|
||||||
void buildEntitySummary();
|
|
||||||
void buildDebrisSingle();
|
|
||||||
void refreshEntityStats();
|
|
||||||
void hideAllWidgets();
|
|
||||||
// Summed remaining scrap across the selected debris (REQ-UI-DEBRIS-PANEL).
|
|
||||||
int selectedDebrisScrapTotal() const;
|
|
||||||
// "Scrap x N" line for the multi-object summary (REQ-UI-FIELD-MULTI-SELECTION).
|
|
||||||
QString scrapTotalText() const;
|
|
||||||
|
|
||||||
Simulation* m_sim;
|
|
||||||
const GameConfig* m_config;
|
|
||||||
|
|
||||||
bool m_debugDraw = false;
|
|
||||||
|
|
||||||
// The selected ships/defence stations. Shares the "field" selection category with
|
|
||||||
// debris (m_selectedDebris): both can be non-empty at once (REQ-UI-SELECTION-CATEGORIES).
|
|
||||||
std::vector<entt::entity> m_selectedEntities;
|
|
||||||
std::vector<entt::entity> m_selectedDebris;
|
|
||||||
|
|
||||||
QVBoxLayout* m_layout;
|
|
||||||
QLabel* m_entityTitleLabel;
|
|
||||||
ShipStatsPanel* m_entityStatsPanel;
|
|
||||||
QLabel* m_stationStatsLabel;
|
|
||||||
QLabel* m_entitySummaryLabel;
|
|
||||||
// Shows the debris "Scrap" stat row (single selection) — the scrap total for the
|
|
||||||
// multi-object summary lives in m_entitySummaryLabel instead.
|
|
||||||
QLabel* m_scrapLabel;
|
|
||||||
};
|
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "SelectionPanel.h"
|
#include "SelectionPanel.h"
|
||||||
#include "ShipLayoutBlueprintSerializer.h"
|
#include "ShipLayoutBlueprintSerializer.h"
|
||||||
#include "ShipLayoutDialog.h"
|
#include "ShipLayoutDialog.h"
|
||||||
|
#include "BuildingIconCache.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
#include "ModalPauseScope.h"
|
#include "ModalPauseScope.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
@@ -48,31 +49,28 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|||||||
setWindowTitle(tr("Dota Factory"));
|
setWindowTitle(tr("Dota Factory"));
|
||||||
resize(1280, 768);
|
resize(1280, 768);
|
||||||
|
|
||||||
// Item icons live alongside the config (a sibling of the config dir), read from
|
// Item and building icons live alongside the config (siblings of the config dir),
|
||||||
// disk at runtime like the building icons and visuals.toml (REQ-UI-ITEM-ICON).
|
// read from disk at runtime the same way visuals.toml is (REQ-UI-ITEM-ICON,
|
||||||
const std::string itemsIconDir = QDir::cleanPath(
|
// REQ-UI-BUILD-ICON).
|
||||||
QString::fromStdString(m_configDir) + "/../icons/items").toStdString();
|
const QString configDirPath = QString::fromStdString(m_configDir);
|
||||||
|
|
||||||
m_itemIcons = std::make_unique<ItemIconCache>(
|
m_itemIcons = std::make_unique<ItemIconCache>(
|
||||||
QString::fromStdString(itemsIconDir));
|
QDir::cleanPath(configDirPath + "/../icons/items"));
|
||||||
|
m_buildingIcons = std::make_unique<BuildingIconCache>(
|
||||||
|
QDir::cleanPath(configDirPath + "/../icons/buildings"));
|
||||||
|
|
||||||
m_headerBar = new HeaderBar(sim, &sim->getConfig(), m_itemIcons.get(), this);
|
m_headerBar = new HeaderBar(sim, &sim->getConfig(), m_itemIcons.get(), this);
|
||||||
|
|
||||||
m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
|
m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
|
||||||
m_itemIcons.get(), m_replay.get(), this);
|
m_itemIcons.get(), m_replay.get(), this);
|
||||||
|
|
||||||
// Building icons live alongside the config (a sibling of the config dir), read
|
|
||||||
// from disk at runtime the same way visuals.toml is.
|
|
||||||
const std::string iconDir = QDir::cleanPath(
|
|
||||||
QString::fromStdString(m_configDir) + "/../icons/buildings").toStdString();
|
|
||||||
|
|
||||||
// Floats over the game world at its bottom center, sized to its buttons
|
// Floats over the game world at its bottom center, sized to its buttons
|
||||||
// (REQ-UI-BUILD-BAR). Creation order is the stacking order for siblings, so
|
// (REQ-UI-BUILD-BAR). Creation order is the stacking order for siblings, so
|
||||||
// building it after the world view puts it above the world and its vignettes,
|
// building it after the world view puts it above the world and its vignettes,
|
||||||
// and before the dim overlay keeps modals dimming it too (REQ-UI-MODAL-DIM).
|
// and before the dim overlay keeps modals dimming it too (REQ-UI-MODAL-DIM).
|
||||||
// Its geometry comes from layoutPanels().
|
// Its geometry comes from layoutPanels().
|
||||||
m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(), iconDir,
|
m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(),
|
||||||
m_itemIcons.get(), this);
|
m_buildingIcons.get(), m_itemIcons.get(),
|
||||||
|
this);
|
||||||
|
|
||||||
// The blueprints have no widget of their own: they are saved with Ctrl+C and picked
|
// The blueprints have no widget of their own: they are saved with Ctrl+C and picked
|
||||||
// from a modal dialog (REQ-UI-BLUEPRINT-DIALOG), both driven from this window
|
// from a modal dialog (REQ-UI-BLUEPRINT-DIALOG), both driven from this window
|
||||||
@@ -85,7 +83,9 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|||||||
// the build button bar it is a sibling of the world view built after it, which is
|
// the build button bar it is a sibling of the world view built after it, which is
|
||||||
// what puts it above the world and its vignettes and below the dim overlay. It
|
// what puts it above the world and its vignettes and below the dim overlay. It
|
||||||
// brings its own chrome; its geometry comes from layoutPanels().
|
// brings its own chrome; its geometry comes from layoutPanels().
|
||||||
m_selectionPanel = new SelectionPanel(sim, &sim->getConfig(), this);
|
m_selectionPanel = new SelectionPanel(sim, &sim->getConfig(), &m_visuals,
|
||||||
|
m_itemIcons.get(), m_buildingIcons.get(),
|
||||||
|
this);
|
||||||
|
|
||||||
// Created last so it stacks above the other children; covers the whole window and
|
// Created last so it stacks above the other children; covers the whole window and
|
||||||
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
|
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class HeaderBar;
|
|||||||
class SelectionPanel;
|
class SelectionPanel;
|
||||||
class BuildButtonBar;
|
class BuildButtonBar;
|
||||||
class BlueprintLibrary;
|
class BlueprintLibrary;
|
||||||
|
class BuildingIconCache;
|
||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
class QCloseEvent;
|
class QCloseEvent;
|
||||||
class QResizeEvent;
|
class QResizeEvent;
|
||||||
@@ -92,6 +93,9 @@ private:
|
|||||||
// One per-item icon cache for the whole window (REQ-UI-ITEM-ICON): the header,
|
// One per-item icon cache for the whole window (REQ-UI-ITEM-ICON): the header,
|
||||||
// build bar, world view, and recipe dialog all rasterize the same SVGs.
|
// build bar, world view, and recipe dialog all rasterize the same SVGs.
|
||||||
std::unique_ptr<ItemIconCache> m_itemIcons;
|
std::unique_ptr<ItemIconCache> m_itemIcons;
|
||||||
|
// Likewise one per-building chip cache (REQ-UI-BUILD-ICON), shared by the build bar
|
||||||
|
// and the selection panel's card headers (REQ-UI-SELECTION-CARD).
|
||||||
|
std::unique_ptr<BuildingIconCache> m_buildingIcons;
|
||||||
GameWorldView* m_gameWorldView;
|
GameWorldView* m_gameWorldView;
|
||||||
HeaderBar* m_headerBar;
|
HeaderBar* m_headerBar;
|
||||||
SelectionPanel* m_selectionPanel;
|
SelectionPanel* m_selectionPanel;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,48 +1,34 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <QPoint>
|
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "DebrisSelectionChangedEvent.h"
|
||||||
#include "Building.h"
|
#include "DebugDrawToggledEvent.h"
|
||||||
#include "BuildingId.h"
|
|
||||||
#include "EntitySelectionChangedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
|
||||||
#include "PlayerCommandsAppliedEvent.h"
|
#include "PlayerCommandsAppliedEvent.h"
|
||||||
#include "RecipesConfig.h"
|
|
||||||
#include "DebrisSelectionChangedEvent.h"
|
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "ShipLayout.h"
|
|
||||||
#include "ShipsConfig.h"
|
|
||||||
#include "Tick.h"
|
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
|
#include "selection/SelectionContentFactory.h"
|
||||||
|
#include "selection/SelectionContext.h"
|
||||||
|
|
||||||
|
struct GameConfig;
|
||||||
|
struct VisualsConfig;
|
||||||
|
class BuildingIconCache;
|
||||||
|
class ItemIconCache;
|
||||||
|
class SelectionContent;
|
||||||
class Simulation;
|
class Simulation;
|
||||||
class FieldSelectionPanel;
|
|
||||||
class ShipLayoutPreview;
|
|
||||||
class QLabel;
|
|
||||||
class QListWidget;
|
|
||||||
class QPushButton;
|
|
||||||
class QScrollArea;
|
class QScrollArea;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
|
||||||
// Shows the current selection. The building category (buildings and construction sites)
|
// Shows the current selection. The panel itself renders nothing: it arbitrates between
|
||||||
// is rendered by this panel itself; the field category (ships, defence stations, debris)
|
// the two selection categories (REQ-UI-SELECTION-CATEGORIES), picks the card that fits
|
||||||
// is rendered by the embedded FieldSelectionPanel.
|
// what is selected (REQ-UI-SELECTION-CONTENT), and hosts exactly one of them at a time.
|
||||||
//
|
// What each card looks like lives in src/ui/selection/.
|
||||||
// The two categories are mutually exclusive (REQ-UI-SELECTION-CATEGORIES) and this panel
|
|
||||||
// is the sole arbiter of which one owns the content: it listens to all three selection
|
|
||||||
// events, forwards the field ones to the child panel, and drops the losing category's
|
|
||||||
// content. Neither panel touches the other's widgets.
|
|
||||||
//
|
//
|
||||||
// The panel floats over the game world view rather than occupying a column of its own
|
// The panel floats over the game world view rather than occupying a column of its own
|
||||||
// (REQ-UI-SELECTION-PANEL): it sizes itself to its content, anchors to the right edge of
|
// (REQ-UI-SELECTION-PANEL): it sizes itself to its card, anchors to the right edge of
|
||||||
// the band its owner hands it, and hides itself entirely while nothing is selected
|
// the band its owner hands it, and hides itself entirely while nothing is selected
|
||||||
// (REQ-UI-EMPTY-SELECTION).
|
// (REQ-UI-EMPTY-SELECTION).
|
||||||
class SelectionPanel : public QWidget,
|
class SelectionPanel : public QWidget,
|
||||||
@@ -50,13 +36,17 @@ class SelectionPanel : public QWidget,
|
|||||||
PlayerCommandsAppliedEvent,
|
PlayerCommandsAppliedEvent,
|
||||||
EntitySelectionChangedEvent,
|
EntitySelectionChangedEvent,
|
||||||
SelectionChangedEvent,
|
SelectionChangedEvent,
|
||||||
DebrisSelectionChangedEvent>
|
DebrisSelectionChangedEvent,
|
||||||
|
DebugDrawToggledEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// visuals, itemIcons and buildingIcons are window-wide rendering resources the cards
|
||||||
|
// draw from; none is owned and all must outlive this widget.
|
||||||
SelectionPanel(Simulation* sim, const GameConfig* config,
|
SelectionPanel(Simulation* sim, const GameConfig* config,
|
||||||
QWidget* parent = nullptr);
|
const VisualsConfig* visuals, ItemIconCache* itemIcons,
|
||||||
|
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
||||||
~SelectionPanel() override;
|
~SelectionPanel() override;
|
||||||
|
|
||||||
// Confines the panel to the given band of the game world view: it right-aligns
|
// Confines the panel to the given band of the game world view: it right-aligns
|
||||||
@@ -66,86 +56,41 @@ public:
|
|||||||
void anchorTo(const QRect& bandRect);
|
void anchorTo(const QRect& bandRect);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
|
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
|
||||||
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
|
|
||||||
|
|
||||||
private slots:
|
// Re-reads the live values of the card on screen. When the selection now calls for a
|
||||||
void onSelectRecipeClicked();
|
// different card -- a construction site finishing is the case that matters -- it
|
||||||
void onClearBelt();
|
// rebuilds instead, because a card never changes its own shape.
|
||||||
void onSplitterFilterChanged();
|
void refreshContent();
|
||||||
|
// Replaces the card with the one the current selection calls for.
|
||||||
private:
|
void rebuildContent();
|
||||||
// Why the selection display is being refreshed. A periodic tick only needs a
|
// Shows the panel while a card exists and hides it otherwise
|
||||||
// lightweight content update (e.g. a construction site's progress label),
|
// (REQ-UI-EMPTY-SELECTION), re-fitting it to the card while it is shown.
|
||||||
// whereas an applied player command may have changed the configuration and
|
|
||||||
// needs a full structural rebuild.
|
|
||||||
enum class RefreshReason
|
|
||||||
{
|
|
||||||
PeriodicTick,
|
|
||||||
CommandApplied
|
|
||||||
};
|
|
||||||
|
|
||||||
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
|
||||||
// Gives the panel to the field category once it has anything selected.
|
|
||||||
void yieldToFieldSelection();
|
|
||||||
// Shows the panel while either category has a selection and hides it otherwise
|
|
||||||
// (REQ-UI-EMPTY-SELECTION), re-fitting it to its current content while it is shown.
|
|
||||||
// Every path that changes the content ends here, because the content is what sizes
|
|
||||||
// the panel.
|
|
||||||
void updateVisibility();
|
void updateVisibility();
|
||||||
// Re-fits the panel to its content within the anchored band.
|
// Re-fits the panel to its card within the anchored band.
|
||||||
void refit();
|
void refit();
|
||||||
void refreshSelectionDisplay(RefreshReason reason);
|
|
||||||
void rebuild();
|
|
||||||
void hideAllWidgets();
|
|
||||||
void buildEmpty();
|
|
||||||
void buildSingle(BuildingId id);
|
|
||||||
void buildMulti(const std::vector<BuildingId>& ids);
|
|
||||||
void refreshBuffers(const Building* b);
|
|
||||||
void refreshSiteProgress(const ConstructionSite* s);
|
|
||||||
void updateShipyardLayoutWidgets(BuildingType type,
|
|
||||||
const std::string& recipeId,
|
|
||||||
const std::optional<ShipLayoutConfig>& shipLayout);
|
|
||||||
void buildSplitterFilters(const std::optional<BeltSystem::SplitterInfo>& info);
|
|
||||||
const RecipeDef* findRecipe(const Building* b) const;
|
|
||||||
const ShipDef* findShipDef(const std::string& id) const;
|
|
||||||
std::vector<std::string> getAllItemIds() const;
|
|
||||||
|
|
||||||
Simulation* m_sim;
|
SelectionContext m_context;
|
||||||
const GameConfig* m_config;
|
// Read through m_context by the cards that need it, so a toggle reaches the card on
|
||||||
std::vector<BuildingId> m_selectedBuildingIds;
|
// screen without it having to subscribe to the event itself.
|
||||||
|
bool m_debugDrawEnabled = false;
|
||||||
|
|
||||||
|
SelectionRequest m_request;
|
||||||
|
ContentKey m_contentKey;
|
||||||
|
SelectionContent* m_content = nullptr;
|
||||||
|
|
||||||
// The band the panel confines itself to, in the coordinates of its parent; null
|
// The band the panel confines itself to, in the coordinates of its parent; null
|
||||||
// until the owner has anchored it for the first time.
|
// until the owner has anchored it for the first time.
|
||||||
QRect m_bandRect;
|
QRect m_bandRect;
|
||||||
|
|
||||||
// Scrolls the content once it outgrows the band (REQ-UI-SELECTION-PANEL). All the
|
// Scrolls the card once it outgrows the band (REQ-UI-SELECTION-PANEL). The card is a
|
||||||
// content widgets below are children of m_content, not of the panel itself.
|
// child of m_body, not of the panel itself.
|
||||||
QScrollArea* m_scrollArea;
|
QScrollArea* m_scrollArea;
|
||||||
QWidget* m_content;
|
QWidget* m_body;
|
||||||
|
QVBoxLayout* m_bodyLayout;
|
||||||
QVBoxLayout* m_layout;
|
|
||||||
QLabel* m_titleLabel;
|
|
||||||
QPushButton* m_recipeSelectButton;
|
|
||||||
QPushButton* m_clearBeltBtn;
|
|
||||||
QLabel* m_filterALabel;
|
|
||||||
QListWidget* m_filterAList;
|
|
||||||
QLabel* m_filterBLabel;
|
|
||||||
QListWidget* m_filterBList;
|
|
||||||
QLabel* m_buffersLabel;
|
|
||||||
|
|
||||||
ShipLayoutPreview* m_layoutPreview;
|
|
||||||
QPushButton* m_configureLayoutBtn;
|
|
||||||
|
|
||||||
std::optional<BuildingId> m_singleBuildingId;
|
|
||||||
bool m_singleIsSite = false; // selected single entity is a construction site
|
|
||||||
QPoint m_splitterTile;
|
|
||||||
std::string m_currentRecipeId;
|
|
||||||
|
|
||||||
// Renders the field selection (actors + debris) below the building content
|
|
||||||
// (REQ-UI-FIELD-MULTI-SELECTION). Hides itself while nothing field-side is selected.
|
|
||||||
FieldSelectionPanel* m_fieldSelectionPanel;
|
|
||||||
};
|
};
|
||||||
|
|||||||
55
src/ui/selection/AutoProductionContent.cpp
Normal file
55
src/ui/selection/AutoProductionContent.cpp
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#include "AutoProductionContent.h"
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingTarget.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
|
|
||||||
|
AutoProductionContent::AutoProductionContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request,
|
||||||
|
QWidget* parent)
|
||||||
|
: BufferedBuildingContent(context, request.buildings.front(), parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void AutoProductionContent::refreshConfiguration()
|
||||||
|
{
|
||||||
|
const BuildingTarget target = resolveBuildingTarget(getContext(), getBuildingId());
|
||||||
|
if (!target.isValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedBuildingContent::CycleInfo AutoProductionContent::getCycleInfo(
|
||||||
|
const Building& building) const
|
||||||
|
{
|
||||||
|
CycleInfo info;
|
||||||
|
// An auto-recipe building always runs an implicit recipe (REQ-BLD-SMELTER,
|
||||||
|
// REQ-BLD-REPROCESSING), so its production section is always shown -- but only a
|
||||||
|
// running cycle names a recipe, so while it is idle there is no cycle time.
|
||||||
|
info.runsProduction = true;
|
||||||
|
if (!building.production.has_value())
|
||||||
|
{
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecipeDef* recipe = getContext().config->recipes.findRecipeDef(
|
||||||
|
building.production->recipeId, building.type);
|
||||||
|
if (!recipe)
|
||||||
|
{
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RecipeIngredient& ingredient : recipe->inputs)
|
||||||
|
{
|
||||||
|
info.perCycleInputs[ingredient.item] = ingredient.amount;
|
||||||
|
}
|
||||||
|
for (const RecipeOutput& output : recipe->outputs)
|
||||||
|
{
|
||||||
|
info.perCycleOutputs[output.item] = output.amount;
|
||||||
|
}
|
||||||
|
info.durationSeconds = recipe->durationSeconds;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
21
src/ui/selection/AutoProductionContent.h
Normal file
21
src/ui/selection/AutoProductionContent.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BufferedBuildingContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
// The card for a Smelter or a Reprocessing Plant (REQ-UI-SELECTION-CONTENT). Both
|
||||||
|
// auto-process whatever they receive and have no player-facing recipe selection
|
||||||
|
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), so the card has no configuration group at
|
||||||
|
// all; its cycle is whichever recipe is currently in production.
|
||||||
|
class AutoProductionContent : public BufferedBuildingContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
AutoProductionContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshConfiguration() override;
|
||||||
|
CycleInfo getCycleInfo(const Building& building) const override;
|
||||||
|
};
|
||||||
38
src/ui/selection/BeltContent.cpp
Normal file
38
src/ui/selection/BeltContent.cpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#include "BeltContent.h"
|
||||||
|
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BuildingTarget.h"
|
||||||
|
#include "ClearBeltControl.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
|
|
||||||
|
BeltContent::BeltContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent)
|
||||||
|
// Only an operational tile aggregates, so a card showing several is never a site;
|
||||||
|
// a single selected tile still can be one.
|
||||||
|
: SelectionContent(context,
|
||||||
|
request.buildings.size() == 1
|
||||||
|
? asConstructionSite(context, request.buildings.front())
|
||||||
|
: std::nullopt,
|
||||||
|
parent)
|
||||||
|
, m_ids(request.buildings)
|
||||||
|
{
|
||||||
|
getRuntimeLayout()->addWidget(new ClearBeltControl(context, m_ids, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeltContent::refreshConfiguration()
|
||||||
|
{
|
||||||
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_ids.front());
|
||||||
|
if (!target.isValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An aggregated selection may mix belts with tunnel ends, so it is named after the
|
||||||
|
// first tile; the count says how many are held (REQ-UI-SELECTION-AGGREGATE).
|
||||||
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
||||||
|
if (m_ids.size() > 1)
|
||||||
|
{
|
||||||
|
setCountSlot(static_cast<int>(m_ids.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/ui/selection/BeltContent.h
Normal file
30
src/ui/selection/BeltContent.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "SelectionContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
// The card for a belt, a tunnel entry or a tunnel exit
|
||||||
|
// (REQ-UI-SELECTION-CONTENT). These carry no configuration and no buffers of their own;
|
||||||
|
// their card is the clear action alone (REQ-UI-BELT-CLEAR).
|
||||||
|
//
|
||||||
|
// Because that action already operates on the whole selection, several of them aggregate
|
||||||
|
// into this one card with the count in the header (REQ-UI-SELECTION-AGGREGATE) -- the
|
||||||
|
// splitter is not among them, as its output filters are per-object.
|
||||||
|
class BeltContent : public SelectionContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
BeltContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshConfiguration() override;
|
||||||
|
void refreshRuntime() override {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<BuildingId> m_ids;
|
||||||
|
};
|
||||||
95
src/ui/selection/BufferSection.cpp
Normal file
95
src/ui/selection/BufferSection.cpp
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#include "BufferSection.h"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// One "<item>: <count>[/<per cycle>]" entry.
|
||||||
|
QString formatEntry(const std::string& itemId, int count, int perCycle)
|
||||||
|
{
|
||||||
|
QString text = QString::fromStdString(itemId) + ": " + QString::number(count);
|
||||||
|
if (perCycle > 0)
|
||||||
|
{
|
||||||
|
text += "/" + QString::number(perCycle);
|
||||||
|
}
|
||||||
|
return text + " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
int findPerCycle(const std::map<std::string, int>& perCycle, const std::string& itemId)
|
||||||
|
{
|
||||||
|
const std::map<std::string, int>::const_iterator it = perCycle.find(itemId);
|
||||||
|
return (it != perCycle.end()) ? it->second : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
BufferSection::BufferSection(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
|
||||||
|
m_label = new QLabel(this);
|
||||||
|
m_label->setWordWrap(true);
|
||||||
|
layout->addWidget(m_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BufferSection::setBuffers(const Building& building,
|
||||||
|
const std::map<std::string, int>& perCycleInputs,
|
||||||
|
const std::map<std::string, int>& perCycleOutputs)
|
||||||
|
{
|
||||||
|
QString text;
|
||||||
|
|
||||||
|
if (!building.inputBuffer.counts.empty())
|
||||||
|
{
|
||||||
|
text += tr("Input: ");
|
||||||
|
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
|
||||||
|
{
|
||||||
|
text += formatEntry(entry.first.id, entry.second,
|
||||||
|
findPerCycle(perCycleInputs, entry.first.id));
|
||||||
|
}
|
||||||
|
text += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output-side items are the buffered ones 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> outputCounts;
|
||||||
|
for (const Item& item : building.outputBuffer.items)
|
||||||
|
{
|
||||||
|
outputCounts[item.type.id]++;
|
||||||
|
}
|
||||||
|
for (const std::vector<BeltItemSlot>& lane : building.emergingItems)
|
||||||
|
{
|
||||||
|
for (const BeltItemSlot& slot : lane)
|
||||||
|
{
|
||||||
|
outputCounts[slot.item.type.id]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A configured building lists every item its cycle produces, so an output the player
|
||||||
|
// is waiting for shows as 0 rather than being absent.
|
||||||
|
for (const std::pair<const std::string, int>& entry : perCycleOutputs)
|
||||||
|
{
|
||||||
|
outputCounts.emplace(entry.first, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputCounts.empty())
|
||||||
|
{
|
||||||
|
text += tr("Output: ");
|
||||||
|
for (const std::pair<const std::string, int>& entry : outputCounts)
|
||||||
|
{
|
||||||
|
text += formatEntry(entry.first, entry.second,
|
||||||
|
findPerCycle(perCycleOutputs, entry.first));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_label->setText(text.trimmed());
|
||||||
|
setVisible(!text.trimmed().isEmpty());
|
||||||
|
}
|
||||||
31
src/ui/selection/BufferSection.h
Normal file
31
src/ui/selection/BufferSection.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
struct Building;
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// The input and output buffer contents of one building (REQ-UI-SINGLE-SELECTION).
|
||||||
|
//
|
||||||
|
// Counting what is in the buffers is the same for every building type, so it happens
|
||||||
|
// here; what a cycle consumes and produces is not, so the owning content supplies those
|
||||||
|
// per-cycle amounts. An item with no entry in the maps is shown without a denominator.
|
||||||
|
class BufferSection : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BufferSection(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// perCycleInputs and perCycleOutputs map an item id to the amount one production
|
||||||
|
// cycle consumes or produces. Both may be empty, for a building that runs no cycle.
|
||||||
|
void setBuffers(const Building& building,
|
||||||
|
const std::map<std::string, int>& perCycleInputs,
|
||||||
|
const std::map<std::string, int>& perCycleOutputs);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_label;
|
||||||
|
};
|
||||||
39
src/ui/selection/BufferedBuildingContent.cpp
Normal file
39
src/ui/selection/BufferedBuildingContent.cpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#include "BufferedBuildingContent.h"
|
||||||
|
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BufferSection.h"
|
||||||
|
#include "BuildingTarget.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "ProductionSection.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context,
|
||||||
|
BuildingId id, QWidget* parent)
|
||||||
|
: SelectionContent(context, asConstructionSite(context, id), parent)
|
||||||
|
, m_id(id)
|
||||||
|
{
|
||||||
|
m_buffers = new BufferSection(this);
|
||||||
|
m_production = new ProductionSection(this);
|
||||||
|
getRuntimeLayout()->addWidget(m_buffers);
|
||||||
|
getRuntimeLayout()->addWidget(m_production);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BufferedBuildingContent::refreshRuntime()
|
||||||
|
{
|
||||||
|
const Building* building = findBuilding(getContext().sim->getFactoryState(), m_id);
|
||||||
|
if (!building)
|
||||||
|
{
|
||||||
|
// Gone under the card. SelectionPanel rebuilds on the same refresh; this only
|
||||||
|
// has to avoid reading it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProductionStatusSlot(*building);
|
||||||
|
|
||||||
|
const CycleInfo cycle = getCycleInfo(*building);
|
||||||
|
m_buffers->setBuffers(*building, cycle.perCycleInputs, cycle.perCycleOutputs);
|
||||||
|
m_production->setProduction(cycle.runsProduction, *building, cycle.durationSeconds,
|
||||||
|
getContext().sim->getCurrentTick());
|
||||||
|
}
|
||||||
53
src/ui/selection/BufferedBuildingContent.h
Normal file
53
src/ui/selection/BufferedBuildingContent.h
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "SelectionContent.h"
|
||||||
|
|
||||||
|
struct Building;
|
||||||
|
class BufferSection;
|
||||||
|
class ProductionSection;
|
||||||
|
|
||||||
|
// Shared body of the four cards that show one building with buffers -- the Miner and
|
||||||
|
// Assembler, the Smelter and Reprocessing Plant, the Shipyard, and the Salvage Bay
|
||||||
|
// (REQ-UI-SELECTION-CONTENT). All four show the same header status, buffer contents and
|
||||||
|
// production progress; they differ only in what one production cycle costs and how long
|
||||||
|
// it takes, which is what the subclass supplies.
|
||||||
|
//
|
||||||
|
// This is implementation sharing, not a catalog entry: every concrete subclass is one
|
||||||
|
// row of the content catalog.
|
||||||
|
class BufferedBuildingContent : public SelectionContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// What one production cycle of this building consumes, produces, and takes.
|
||||||
|
struct CycleInfo
|
||||||
|
{
|
||||||
|
std::map<std::string, int> perCycleInputs;
|
||||||
|
std::map<std::string, int> perCycleOutputs;
|
||||||
|
// False when the building produces nothing at all (the Salvage Bay,
|
||||||
|
// REQ-BLD-SALVAGE-BAY) or has no recipe or schematic selected yet: the
|
||||||
|
// production section is then not shown (REQ-UI-PRODUCTION-PROGRESS).
|
||||||
|
bool runsProduction = false;
|
||||||
|
// 0 while an auto-recipe building sits between cycles, when no single recipe
|
||||||
|
// names a cycle time; the progress line then reads "idle".
|
||||||
|
double durationSeconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
BufferedBuildingContent(const SelectionContext& context, BuildingId id,
|
||||||
|
QWidget* parent);
|
||||||
|
|
||||||
|
virtual CycleInfo getCycleInfo(const Building& building) const = 0;
|
||||||
|
|
||||||
|
BuildingId getBuildingId() const { return m_id; }
|
||||||
|
|
||||||
|
void refreshRuntime() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BuildingId m_id;
|
||||||
|
BufferSection* m_buffers;
|
||||||
|
ProductionSection* m_production;
|
||||||
|
};
|
||||||
34
src/ui/selection/BuildingTarget.cpp
Normal file
34
src/ui/selection/BuildingTarget.cpp
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#include "BuildingTarget.h"
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "SelectionContext.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
BuildingTarget resolveBuildingTarget(const SelectionContext& context, BuildingId id)
|
||||||
|
{
|
||||||
|
BuildingTarget target;
|
||||||
|
target.building = findBuilding(context.sim->getFactoryState(), id);
|
||||||
|
target.site = target.building
|
||||||
|
? nullptr
|
||||||
|
: findSite(context.sim->getFactoryState(), id);
|
||||||
|
if (!target.isValid())
|
||||||
|
{
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.type = target.building ? target.building->type : target.site->type;
|
||||||
|
target.recipeId = target.building ? target.building->recipeId : target.site->recipeId;
|
||||||
|
target.shipLayout = target.building ? target.building->shipLayout : target.site->shipLayout;
|
||||||
|
target.anchor = target.building ? target.building->anchor : target.site->anchor;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BuildingId> asConstructionSite(const SelectionContext& context,
|
||||||
|
BuildingId id)
|
||||||
|
{
|
||||||
|
if (findSite(context.sim->getFactoryState(), id) != nullptr)
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
45
src/ui/selection/BuildingTarget.h
Normal file
45
src/ui/selection/BuildingTarget.h
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
|
struct Building;
|
||||||
|
struct ConstructionSite;
|
||||||
|
struct SelectionContext;
|
||||||
|
|
||||||
|
// One selected building id resolved to whichever of the two things it names: an
|
||||||
|
// operational building or a construction site still queued or under construction. A
|
||||||
|
// site carries the same configuration as the building it will become
|
||||||
|
// (REQ-BLD-SITE-CONFIG), so the fields both have are read out here once instead of in
|
||||||
|
// every content that shows a single building.
|
||||||
|
struct BuildingTarget
|
||||||
|
{
|
||||||
|
const Building* building = nullptr; // null while it is still a site
|
||||||
|
const ConstructionSite* site = nullptr; // null once it is built
|
||||||
|
|
||||||
|
BuildingType type = BuildingType::Miner;
|
||||||
|
std::string recipeId;
|
||||||
|
std::optional<ShipLayoutConfig> shipLayout;
|
||||||
|
QPoint anchor;
|
||||||
|
|
||||||
|
// False when the id names neither -- the object went away under the panel (it was
|
||||||
|
// deconstructed, or its site finished and its id was reused).
|
||||||
|
bool isValid() const { return building != nullptr || site != nullptr; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolves the id against the current factory state. The returned pointers are only
|
||||||
|
// valid until the simulation next mutates, so this is called per refresh rather than
|
||||||
|
// cached.
|
||||||
|
BuildingTarget resolveBuildingTarget(const SelectionContext& context, BuildingId id);
|
||||||
|
|
||||||
|
// The id wrapped as a construction site id when it names one, nullopt when it names an
|
||||||
|
// operational building. Every content showing a single building hands this to
|
||||||
|
// SelectionContent so the base can apply the site rule (REQ-UI-SELECTION-CARD).
|
||||||
|
std::optional<BuildingId> asConstructionSite(const SelectionContext& context,
|
||||||
|
BuildingId id);
|
||||||
60
src/ui/selection/CMakeLists.txt
Normal file
60
src/ui/selection/CMakeLists.txt
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
SET(HDRS
|
||||||
|
${HDRS}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionContext.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionContentFactory.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionNames.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTarget.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DebrisScrap.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BufferedBuildingContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeProductionContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/AutoProductionContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipyardContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/StorageContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/HqContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SplitterContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/MultiBuildingContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/StationContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DebrisContent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FieldMultiContent.h
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
|
|
||||||
|
SET(SRCS
|
||||||
|
${SRCS}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionContentFactory.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionNames.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTarget.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DebrisScrap.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BufferSection.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionSection.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionControl.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ClearBeltControl.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BufferedBuildingContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeProductionContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/AutoProductionContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipyardContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/StorageContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/HqContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SplitterContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/MultiBuildingContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/StationContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DebrisContent.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FieldMultiContent.cpp
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
|
|
||||||
|
set(UI_INCLUDE_PATH
|
||||||
|
${UI_INCLUDE_PATH}
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
PARENT_SCOPE
|
||||||
|
)
|
||||||
54
src/ui/selection/ClearBeltControl.cpp
Normal file
54
src/ui/selection/ClearBeltControl.cpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#include "ClearBeltControl.h"
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "Command.h"
|
||||||
|
#include "CommandRequestedEvent.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
ClearBeltControl::ClearBeltControl(const SelectionContext& context,
|
||||||
|
const std::vector<BuildingId>& ids, QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_context(context)
|
||||||
|
, m_ids(ids)
|
||||||
|
{
|
||||||
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
|
||||||
|
QPushButton* button = new QPushButton(tr("Clear stuck items"), this);
|
||||||
|
layout->addWidget(button);
|
||||||
|
|
||||||
|
connect(button, &QPushButton::clicked, this, [this]() { clearSelectedTiles(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearBeltControl::clearSelectedTiles() const
|
||||||
|
{
|
||||||
|
std::vector<QPoint> tiles;
|
||||||
|
for (BuildingId id : m_ids)
|
||||||
|
{
|
||||||
|
const Building* building = findBuilding(m_context.sim->getFactoryState(), id);
|
||||||
|
if (building && isBeltSubsystemType(building->type))
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : building->bodyCells)
|
||||||
|
{
|
||||||
|
tiles.push_back(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tiles.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<ClearBeltTilesCommand> command =
|
||||||
|
std::make_shared<ClearBeltTilesCommand>();
|
||||||
|
command->tiles = std::move(tiles);
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<CommandRequestedEvent>(command));
|
||||||
|
}
|
||||||
31
src/ui/selection/ClearBeltControl.h
Normal file
31
src/ui/selection/ClearBeltControl.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "SelectionContext.h"
|
||||||
|
|
||||||
|
// The "Clear stuck items" action of the card's runtime group (REQ-UI-BELT-CLEAR): it
|
||||||
|
// removes every item from the selected belt, splitter and tunnel tiles, which is how a
|
||||||
|
// stalled line is resolved.
|
||||||
|
//
|
||||||
|
// It acts on the whole selection rather than on one tile, which is why a selection of
|
||||||
|
// belts and tunnel ends aggregates into a single card instead of a count summary
|
||||||
|
// (REQ-UI-SELECTION-AGGREGATE), and why the count summary shows the action too when a
|
||||||
|
// belt is among the selected buildings.
|
||||||
|
class ClearBeltControl : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
ClearBeltControl(const SelectionContext& context,
|
||||||
|
const std::vector<BuildingId>& ids, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void clearSelectedTiles() const;
|
||||||
|
|
||||||
|
SelectionContext m_context;
|
||||||
|
std::vector<BuildingId> m_ids;
|
||||||
|
};
|
||||||
30
src/ui/selection/DebrisContent.cpp
Normal file
30
src/ui/selection/DebrisContent.cpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#include "DebrisContent.h"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "DebrisScrap.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
DebrisContent::DebrisContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent)
|
||||||
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
|
, m_debris(request.debris)
|
||||||
|
{
|
||||||
|
m_scrapLabel = new QLabel(this);
|
||||||
|
getRuntimeLayout()->addWidget(m_scrapLabel);
|
||||||
|
|
||||||
|
setIdentity(QPixmap(), tr("Debris"));
|
||||||
|
if (m_debris.size() > 1)
|
||||||
|
{
|
||||||
|
setCountSlot(static_cast<int>(m_debris.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebrisContent::refreshRuntime()
|
||||||
|
{
|
||||||
|
// The value falls as the debris is collected and as pieces despawn
|
||||||
|
// (REQ-UI-DEBRIS-CLICK-SELECT), so it is re-summed rather than remembered.
|
||||||
|
m_scrapLabel->setText(tr("Scrap remaining: %1")
|
||||||
|
.arg(sumDebrisScrap(getContext().sim->getAdmin(), m_debris)));
|
||||||
|
}
|
||||||
31
src/ui/selection/DebrisContent.h
Normal file
31
src/ui/selection/DebrisContent.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "SelectionContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// The card for selected debris (REQ-UI-DEBRIS-PANEL): the scrap still left in it.
|
||||||
|
//
|
||||||
|
// It is the field category's aggregating content (REQ-UI-SELECTION-AGGREGATE): several
|
||||||
|
// pieces of debris show this same card with the count in the header and their scrap
|
||||||
|
// summed, because that is the one value the card holds and it adds up.
|
||||||
|
class DebrisContent : public SelectionContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
DebrisContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshRuntime() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<entt::entity> m_debris;
|
||||||
|
QLabel* m_scrapLabel;
|
||||||
|
};
|
||||||
18
src/ui/selection/DebrisScrap.cpp
Normal file
18
src/ui/selection/DebrisScrap.cpp
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#include "DebrisScrap.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "DebrisSystem.h"
|
||||||
|
|
||||||
|
int sumDebrisScrap(const EntityAdmin& admin, const std::vector<entt::entity>& debris)
|
||||||
|
{
|
||||||
|
int total = 0;
|
||||||
|
for (const DebrisInfo& info : getAllDebrisInfo(admin))
|
||||||
|
{
|
||||||
|
if (std::find(debris.begin(), debris.end(), info.entity) != debris.end())
|
||||||
|
{
|
||||||
|
total += info.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
13
src/ui/selection/DebrisScrap.h
Normal file
13
src/ui/selection/DebrisScrap.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
class EntityAdmin;
|
||||||
|
|
||||||
|
// Remaining scrap summed over the given debris, skipping pieces that have already been
|
||||||
|
// collected or despawned (REQ-RES-DEBRIS-DROP). Shared by the debris card and the field
|
||||||
|
// count summary, which show the same total in two shapes (REQ-UI-DEBRIS-PANEL,
|
||||||
|
// REQ-UI-FIELD-MULTI-SELECTION).
|
||||||
|
int sumDebrisScrap(const EntityAdmin& admin, const std::vector<entt::entity>& debris);
|
||||||
91
src/ui/selection/FieldMultiContent.cpp
Normal file
91
src/ui/selection/FieldMultiContent.cpp
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#include "FieldMultiContent.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "DebrisScrap.h"
|
||||||
|
#include "DisplayName.h"
|
||||||
|
#include "EntityAdmin.h"
|
||||||
|
#include "FactionComponent.h"
|
||||||
|
#include "ShipIdentityComponent.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
|
|
||||||
|
FieldMultiContent::FieldMultiContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent)
|
||||||
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
|
, m_actors(request.actors)
|
||||||
|
, m_debris(request.debris)
|
||||||
|
{
|
||||||
|
m_summaryLabel = new QLabel(this);
|
||||||
|
m_summaryLabel->setWordWrap(true);
|
||||||
|
getRuntimeLayout()->addWidget(m_summaryLabel);
|
||||||
|
|
||||||
|
setIdentity(QPixmap(), tr("Mixed selection"));
|
||||||
|
setCountSlot(static_cast<int>(m_actors.size() + m_debris.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldMultiContent::refreshRuntime()
|
||||||
|
{
|
||||||
|
EntityAdmin& admin = getContext().sim->getAdmin();
|
||||||
|
|
||||||
|
// Grouped by faction, kind and ship schematic, in the order the groups are first
|
||||||
|
// seen (REQ-UI-FIELD-MULTI-SELECTION).
|
||||||
|
std::vector<QString> keys;
|
||||||
|
std::map<QString, int> counts;
|
||||||
|
std::map<QString, QString> labels;
|
||||||
|
|
||||||
|
for (entt::entity actor : m_actors)
|
||||||
|
{
|
||||||
|
if (!admin.isValid(actor)) { continue; }
|
||||||
|
const bool isEnemy = admin.hasAll<FactionComponent>(actor)
|
||||||
|
&& admin.get<FactionComponent>(actor).isEnemy;
|
||||||
|
|
||||||
|
QString key;
|
||||||
|
QString label;
|
||||||
|
if (admin.hasAll<ShipIdentityComponent>(actor))
|
||||||
|
{
|
||||||
|
const std::string& id = admin.get<ShipIdentityComponent>(actor).schematicId;
|
||||||
|
const QString name = QString::fromStdString(toDisplayName(id));
|
||||||
|
key = (isEnemy ? QStringLiteral("ship:enemy:")
|
||||||
|
: QStringLiteral("ship:player:"))
|
||||||
|
+ QString::fromStdString(id);
|
||||||
|
label = isEnemy ? tr("Enemy %1").arg(name) : name;
|
||||||
|
}
|
||||||
|
else if (admin.hasAll<StationBodyComponent>(actor))
|
||||||
|
{
|
||||||
|
key = isEnemy ? QStringLiteral("station:enemy")
|
||||||
|
: QStringLiteral("station:player");
|
||||||
|
label = isEnemy ? tr("Enemy Defence Station") : tr("Player Defence Station");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counts.find(key) == counts.end())
|
||||||
|
{
|
||||||
|
keys.push_back(key);
|
||||||
|
labels[key] = label;
|
||||||
|
}
|
||||||
|
counts[key] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList lines;
|
||||||
|
for (const QString& key : keys)
|
||||||
|
{
|
||||||
|
lines << tr("%1 x %2").arg(labels[key]).arg(counts[key]);
|
||||||
|
}
|
||||||
|
if (!m_debris.empty())
|
||||||
|
{
|
||||||
|
lines << tr("Debris x %1").arg(static_cast<int>(m_debris.size()));
|
||||||
|
// The scrap total follows the debris row rather than standing on its own, so it
|
||||||
|
// reads as belonging to it (REQ-UI-DEBRIS-PANEL).
|
||||||
|
lines << tr(" holding %1 scrap").arg(sumDebrisScrap(admin, m_debris));
|
||||||
|
}
|
||||||
|
m_summaryLabel->setText(lines.join('\n'));
|
||||||
|
}
|
||||||
30
src/ui/selection/FieldMultiContent.h
Normal file
30
src/ui/selection/FieldMultiContent.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "SelectionContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// The count summary for a field selection holding more than one object that does not
|
||||||
|
// aggregate (REQ-UI-FIELD-MULTI-SELECTION): several actors, or actors together with
|
||||||
|
// debris. A count per type, and the debris' summed scrap when debris is part of it.
|
||||||
|
class FieldMultiContent : public SelectionContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
FieldMultiContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshRuntime() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<entt::entity> m_actors;
|
||||||
|
std::vector<entt::entity> m_debris;
|
||||||
|
QLabel* m_summaryLabel;
|
||||||
|
};
|
||||||
42
src/ui/selection/HqContent.cpp
Normal file
42
src/ui/selection/HqContent.cpp
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#include "HqContent.h"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "EntityAdmin.h"
|
||||||
|
#include "HealthComponent.h"
|
||||||
|
#include "HqProxyComponent.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
HqContent::HqContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent)
|
||||||
|
// The HQ is placed before the game starts and can never be deconstructed
|
||||||
|
// (REQ-BLD-DECONSTRUCT), so it is never a construction site.
|
||||||
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
|
{
|
||||||
|
m_stockLabel = new QLabel(this);
|
||||||
|
m_hpLabel = new QLabel(this);
|
||||||
|
getRuntimeLayout()->addWidget(m_stockLabel);
|
||||||
|
getRuntimeLayout()->addWidget(m_hpLabel);
|
||||||
|
|
||||||
|
setBuildingIdentity(BuildingType::Hq, getBuildingTypeName(BuildingType::Hq));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HqContent::refreshRuntime()
|
||||||
|
{
|
||||||
|
m_stockLabel->setText(
|
||||||
|
tr("Building blocks: %1").arg(getContext().sim->getBuildingBlocksStock()));
|
||||||
|
|
||||||
|
// The HQ's health lives on its proxy entity, not on the building
|
||||||
|
// (REQ-HQ-STATS, REQ-UI-HP-BARS).
|
||||||
|
EntityAdmin& admin = getContext().sim->getAdmin();
|
||||||
|
admin.forEach<HqProxyComponent, HealthComponent>(
|
||||||
|
[this](entt::entity /*entity*/, const HqProxyComponent& /*proxy*/,
|
||||||
|
const HealthComponent& health)
|
||||||
|
{
|
||||||
|
m_hpLabel->setText(tr("HP: %1 / %2")
|
||||||
|
.arg(static_cast<int>(health.hp + 0.5f))
|
||||||
|
.arg(static_cast<int>(health.maxHp + 0.5f)));
|
||||||
|
});
|
||||||
|
}
|
||||||
29
src/ui/selection/HqContent.h
Normal file
29
src/ui/selection/HqContent.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "SelectionContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// The card for the HQ (REQ-UI-HQ-PANEL): the global building blocks stock and the HQ's
|
||||||
|
// HP. It has no configuration group and no status indicator.
|
||||||
|
//
|
||||||
|
// The stock is not a buffer: blocks delivered by belt go straight into the global stock
|
||||||
|
// (REQ-HQ-BELT-INPUT), which is exactly why the card shows it -- it is what tells the
|
||||||
|
// player to route blocks here. The HP comes from the HQ's proxy entity rather than from
|
||||||
|
// the building, since that is where its health lives.
|
||||||
|
class HqContent : public SelectionContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
HqContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshRuntime() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_stockLabel;
|
||||||
|
QLabel* m_hpLabel;
|
||||||
|
};
|
||||||
90
src/ui/selection/MultiBuildingContent.cpp
Normal file
90
src/ui/selection/MultiBuildingContent.cpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#include "MultiBuildingContent.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "ClearBeltControl.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
MultiBuildingContent::MultiBuildingContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request,
|
||||||
|
QWidget* parent)
|
||||||
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
|
, m_ids(request.buildings)
|
||||||
|
{
|
||||||
|
m_countsLabel = new QLabel(this);
|
||||||
|
m_totalCostLabel = new QLabel(this);
|
||||||
|
getRuntimeLayout()->addWidget(m_countsLabel);
|
||||||
|
getRuntimeLayout()->addWidget(m_totalCostLabel);
|
||||||
|
|
||||||
|
// A selection holding any belt-subsystem tile can still be cleared as a whole
|
||||||
|
// (REQ-UI-BELT-CLEAR), even though the mixture is what kept it from aggregating.
|
||||||
|
bool hasBeltTile = false;
|
||||||
|
for (BuildingId id : m_ids)
|
||||||
|
{
|
||||||
|
const Building* building = findBuilding(context.sim->getFactoryState(), id);
|
||||||
|
if (building && isBeltSubsystemType(building->type))
|
||||||
|
{
|
||||||
|
hasBeltTile = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasBeltTile)
|
||||||
|
{
|
||||||
|
getRuntimeLayout()->addWidget(new ClearBeltControl(context, m_ids, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The header names the size of the selection instead of an object
|
||||||
|
// (REQ-UI-MULTI-SELECTION).
|
||||||
|
setIdentity(QPixmap(), tr("%1 buildings").arg(static_cast<int>(m_ids.size())));
|
||||||
|
|
||||||
|
buildSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiBuildingContent::buildSummary()
|
||||||
|
{
|
||||||
|
std::map<BuildingType, int> counts;
|
||||||
|
for (BuildingId id : m_ids)
|
||||||
|
{
|
||||||
|
const Building* building =
|
||||||
|
findBuilding(getContext().sim->getFactoryState(), id);
|
||||||
|
if (building)
|
||||||
|
{
|
||||||
|
counts[building->type]++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ConstructionSite* site =
|
||||||
|
findSite(getContext().sim->getFactoryState(), id);
|
||||||
|
if (site)
|
||||||
|
{
|
||||||
|
counts[site->type]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList lines;
|
||||||
|
int totalCost = 0;
|
||||||
|
for (const std::pair<const BuildingType, int>& entry : counts)
|
||||||
|
{
|
||||||
|
lines << tr("%1 x %2").arg(getBuildingTypeName(entry.first)).arg(entry.second);
|
||||||
|
|
||||||
|
// Only player-placeable buildings count toward the total; the HQ and defence
|
||||||
|
// stations are excluded (REQ-UI-MULTI-SELECTION). A construction site counts at
|
||||||
|
// its type's full placement cost regardless of progress.
|
||||||
|
const BuildingDef* def =
|
||||||
|
getContext().config->buildings.findBuildingDef(entry.first);
|
||||||
|
if (def && def->playerPlaceable)
|
||||||
|
{
|
||||||
|
totalCost += def->cost * entry.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_countsLabel->setText(lines.join('\n'));
|
||||||
|
m_totalCostLabel->setText(tr("Total: %1 Building Blocks").arg(totalCost));
|
||||||
|
}
|
||||||
34
src/ui/selection/MultiBuildingContent.h
Normal file
34
src/ui/selection/MultiBuildingContent.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "SelectionContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// The count summary for several selected buildings that do not aggregate
|
||||||
|
// (REQ-UI-MULTI-SELECTION, REQ-UI-SELECTION-AGGREGATE): how many of each type, and the
|
||||||
|
// total building block cost of the selection. No per-building detail is shown.
|
||||||
|
class MultiBuildingContent : public SelectionContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
MultiBuildingContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// The summary is fixed for a given selection -- how many of each type were selected,
|
||||||
|
// and what they cost -- so it is built once and has nothing to keep current. A
|
||||||
|
// building leaving the selection re-publishes it and rebuilds this card.
|
||||||
|
void refreshRuntime() override {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void buildSummary();
|
||||||
|
|
||||||
|
std::vector<BuildingId> m_ids;
|
||||||
|
QLabel* m_countsLabel;
|
||||||
|
QLabel* m_totalCostLabel;
|
||||||
|
};
|
||||||
53
src/ui/selection/ProductionSection.cpp
Normal file
53
src/ui/selection/ProductionSection.cpp
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#include "ProductionSection.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
|
||||||
|
ProductionSection::ProductionSection(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
|
||||||
|
m_label = new QLabel(this);
|
||||||
|
layout->addWidget(m_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductionSection::setProduction(bool runsProduction, const Building& building,
|
||||||
|
double durationSeconds, Tick currentTick)
|
||||||
|
{
|
||||||
|
// Nothing selected to produce means neither a cycle time nor a progress indicator
|
||||||
|
// (REQ-UI-PRODUCTION-PROGRESS).
|
||||||
|
if (!runsProduction)
|
||||||
|
{
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString text;
|
||||||
|
if (durationSeconds > 0.0)
|
||||||
|
{
|
||||||
|
text = tr("Cycle: %1 s\n").arg(durationSeconds, 0, 'f', 1);
|
||||||
|
}
|
||||||
|
if (durationSeconds > 0.0 && building.production.has_value())
|
||||||
|
{
|
||||||
|
const Tick cycleTicks = secondsToTicks(durationSeconds);
|
||||||
|
const Tick elapsed =
|
||||||
|
currentTick - (building.production->completesAt - cycleTicks);
|
||||||
|
const int percent = static_cast<int>(
|
||||||
|
std::max(Tick(0), std::min(cycleTicks, elapsed)) * 100 / cycleTicks);
|
||||||
|
text += tr("Progress: %1%").arg(percent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text += tr("Progress: idle");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_label->setText(text);
|
||||||
|
show();
|
||||||
|
}
|
||||||
33
src/ui/selection/ProductionSection.h
Normal file
33
src/ui/selection/ProductionSection.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
struct Building;
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// The cycle time and production progress of one building (REQ-UI-PRODUCTION-PROGRESS).
|
||||||
|
//
|
||||||
|
// Reading the progress off an active cycle is the same for every building type, so it
|
||||||
|
// happens here; working out how long that cycle is differs per type (a recipe's
|
||||||
|
// duration, a schematic's production time plus its modules'), so the owning content
|
||||||
|
// supplies it.
|
||||||
|
class ProductionSection : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ProductionSection(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// runsProduction false hides the section entirely -- the building produces nothing
|
||||||
|
// (a Salvage Bay) or has no recipe or schematic selected yet. When it is true but
|
||||||
|
// durationSeconds is 0 or less, the building is between cycles with no single recipe
|
||||||
|
// to name a cycle time for (an idle auto-recipe building), so the progress line
|
||||||
|
// reads "idle" and the cycle time is left out.
|
||||||
|
void setProduction(bool runsProduction, const Building& building,
|
||||||
|
double durationSeconds, Tick currentTick);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_label;
|
||||||
|
};
|
||||||
59
src/ui/selection/RecipeProductionContent.cpp
Normal file
59
src/ui/selection/RecipeProductionContent.cpp
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#include "RecipeProductionContent.h"
|
||||||
|
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingTarget.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "RecipeSelectionControl.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
|
|
||||||
|
RecipeProductionContent::RecipeProductionContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request,
|
||||||
|
QWidget* parent)
|
||||||
|
: BufferedBuildingContent(context, request.buildings.front(), parent)
|
||||||
|
{
|
||||||
|
const BuildingTarget target = resolveBuildingTarget(context, getBuildingId());
|
||||||
|
|
||||||
|
// The control is shown for a construction site too: a site is configured exactly
|
||||||
|
// like the building it will become (REQ-BLD-SITE-CONFIG).
|
||||||
|
m_recipeControl = new RecipeSelectionControl(context, getBuildingId(), target.type,
|
||||||
|
this);
|
||||||
|
getConfigurationLayout()->addWidget(m_recipeControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecipeProductionContent::refreshConfiguration()
|
||||||
|
{
|
||||||
|
const BuildingTarget target = resolveBuildingTarget(getContext(), getBuildingId());
|
||||||
|
if (!target.isValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
||||||
|
m_recipeControl->setRecipeId(target.recipeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedBuildingContent::CycleInfo RecipeProductionContent::getCycleInfo(
|
||||||
|
const Building& building) const
|
||||||
|
{
|
||||||
|
CycleInfo info;
|
||||||
|
const RecipeDef* recipe = building.recipeId.empty()
|
||||||
|
? nullptr
|
||||||
|
: getContext().config->recipes.findRecipeDef(building.recipeId, building.type);
|
||||||
|
if (!recipe)
|
||||||
|
{
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RecipeIngredient& ingredient : recipe->inputs)
|
||||||
|
{
|
||||||
|
info.perCycleInputs[ingredient.item] = ingredient.amount;
|
||||||
|
}
|
||||||
|
for (const RecipeOutput& output : recipe->outputs)
|
||||||
|
{
|
||||||
|
info.perCycleOutputs[output.item] = output.amount;
|
||||||
|
}
|
||||||
|
info.runsProduction = true;
|
||||||
|
info.durationSeconds = recipe->durationSeconds;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
26
src/ui/selection/RecipeProductionContent.h
Normal file
26
src/ui/selection/RecipeProductionContent.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BufferedBuildingContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
class RecipeSelectionControl;
|
||||||
|
|
||||||
|
// The card for a Miner or an Assembler (REQ-UI-SELECTION-CONTENT): a player-selected
|
||||||
|
// recipe, plus the buffers and production progress every buffered building shows. The
|
||||||
|
// two share a card because both run one selected recipe; a Miner simply has no inputs,
|
||||||
|
// so its input buffer section shows nothing of its own accord.
|
||||||
|
class RecipeProductionContent : public BufferedBuildingContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
RecipeProductionContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshConfiguration() override;
|
||||||
|
CycleInfo getCycleInfo(const Building& building) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
RecipeSelectionControl* m_recipeControl;
|
||||||
|
};
|
||||||
57
src/ui/selection/RecipeSelectionControl.cpp
Normal file
57
src/ui/selection/RecipeSelectionControl.cpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#include "RecipeSelectionControl.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "RecipeSelectionDialog.h"
|
||||||
|
#include "RecipeSelectionRequestedEvent.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
RecipeSelectionControl::RecipeSelectionControl(const SelectionContext& context,
|
||||||
|
BuildingId id, BuildingType type,
|
||||||
|
QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_context(context)
|
||||||
|
, m_id(id)
|
||||||
|
, m_type(type)
|
||||||
|
{
|
||||||
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(0);
|
||||||
|
|
||||||
|
m_button = new QPushButton(this);
|
||||||
|
layout->addWidget(m_button);
|
||||||
|
|
||||||
|
connect(m_button, &QPushButton::clicked, this, [this]() {
|
||||||
|
// Sent synchronously: MainWindow pauses the game, runs the modal dialog, and
|
||||||
|
// restores the speed before this returns. The chosen recipe is only enqueued as
|
||||||
|
// a command though, and drains on a later frame -- so the caption follows from
|
||||||
|
// the next refresh, not from here.
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<RecipeSelectionRequestedEvent>(m_id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecipeSelectionControl::setRecipeId(const std::string& recipeId)
|
||||||
|
{
|
||||||
|
const std::vector<RecipeSelectionOption> options =
|
||||||
|
buildRecipeSelectionOptions(m_type, *m_context.sim, *m_context.config);
|
||||||
|
|
||||||
|
for (const RecipeSelectionOption& option : options)
|
||||||
|
{
|
||||||
|
if (option.id == recipeId && !option.id.empty())
|
||||||
|
{
|
||||||
|
m_button->setText(option.caption);
|
||||||
|
m_button->setToolTip(option.tooltip);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_button->setText(m_type == BuildingType::Shipyard
|
||||||
|
? tr("Select schematic")
|
||||||
|
: tr("Select recipe"));
|
||||||
|
m_button->setToolTip(QString());
|
||||||
|
}
|
||||||
38
src/ui/selection/RecipeSelectionControl.h
Normal file
38
src/ui/selection/RecipeSelectionControl.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "SelectionContext.h"
|
||||||
|
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
|
// The recipe/schematic selection control of the card's configuration group: one button
|
||||||
|
// captioned with the current selection that opens the modal selection dialog
|
||||||
|
// (REQ-UI-SELECT-BUTTON, REQ-UI-CONFIG-INLINE). Used by the Miner and Assembler for
|
||||||
|
// their recipe and by the Shipyard for its schematic; the placeholder caption is the
|
||||||
|
// only difference between the two.
|
||||||
|
//
|
||||||
|
// The control does not apply the choice itself: it asks for the dialog and the choice
|
||||||
|
// arrives back as a command, so the caption follows from the next refresh rather than
|
||||||
|
// from the click.
|
||||||
|
class RecipeSelectionControl : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
RecipeSelectionControl(const SelectionContext& context, BuildingId id,
|
||||||
|
BuildingType type, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
// Re-reads the caption and tooltip for the currently configured recipe id.
|
||||||
|
void setRecipeId(const std::string& recipeId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
SelectionContext m_context;
|
||||||
|
BuildingId m_id;
|
||||||
|
BuildingType m_type;
|
||||||
|
QPushButton* m_button;
|
||||||
|
};
|
||||||
254
src/ui/selection/SelectionContent.cpp
Normal file
254
src/ui/selection/SelectionContent.cpp
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
#include "SelectionContent.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QFont>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QRectF>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BuildingIconCache.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "ProductionRules.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Size of the identity chip in the card header, in device-independent pixels. Smaller
|
||||||
|
// than the build button's 32 px chip: here it labels a line of text rather than being
|
||||||
|
// the whole button face.
|
||||||
|
const int kSymbolSizePx = 20;
|
||||||
|
|
||||||
|
// Diameter of the status dot beside the header's right-slot caption
|
||||||
|
// (REQ-UI-SELECTION-STATUS).
|
||||||
|
const int kStatusDotSizePx = 8;
|
||||||
|
|
||||||
|
// Spacing inside the card and between the header's elements.
|
||||||
|
const int kCardSpacingPx = 6;
|
||||||
|
const int kHeaderSpacingPx = 6;
|
||||||
|
|
||||||
|
QPixmap renderStatusDot(const QColor& fill, const QColor& outline)
|
||||||
|
{
|
||||||
|
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||||
|
QPixmap pixmap(static_cast<int>(kStatusDotSizePx * dpr),
|
||||||
|
static_cast<int>(kStatusDotSizePx * dpr));
|
||||||
|
pixmap.setDevicePixelRatio(dpr);
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
|
||||||
|
QPainter painter(&pixmap);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter.setPen(outline);
|
||||||
|
painter.setBrush(fill);
|
||||||
|
// Inset by half the pen width so the outline stays inside the pixmap.
|
||||||
|
painter.drawEllipse(QRectF(0.5, 0.5, kStatusDotSizePx - 1.0, kStatusDotSizePx - 1.0));
|
||||||
|
return pixmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
SelectionContent::SelectionContent(const SelectionContext& context,
|
||||||
|
std::optional<BuildingId> constructionSiteId,
|
||||||
|
QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_context(context)
|
||||||
|
, m_siteId(constructionSiteId)
|
||||||
|
, m_constructionLabel(nullptr)
|
||||||
|
{
|
||||||
|
QVBoxLayout* cardLayout = new QVBoxLayout(this);
|
||||||
|
cardLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
cardLayout->setSpacing(kCardSpacingPx);
|
||||||
|
|
||||||
|
// Header: identity symbol, name, and the right slot pushed to the far edge
|
||||||
|
// (REQ-UI-SELECTION-CARD).
|
||||||
|
QWidget* header = new QWidget(this);
|
||||||
|
QHBoxLayout* headerLayout = new QHBoxLayout(header);
|
||||||
|
headerLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
headerLayout->setSpacing(kHeaderSpacingPx);
|
||||||
|
|
||||||
|
m_symbolLabel = new QLabel(header);
|
||||||
|
m_symbolLabel->hide();
|
||||||
|
|
||||||
|
m_nameLabel = new QLabel(header);
|
||||||
|
QFont nameFont = m_nameLabel->font();
|
||||||
|
nameFont.setBold(true);
|
||||||
|
m_nameLabel->setFont(nameFont);
|
||||||
|
|
||||||
|
m_slotDot = new QLabel(header);
|
||||||
|
m_slotDot->hide();
|
||||||
|
|
||||||
|
m_slotLabel = new QLabel(header);
|
||||||
|
m_slotLabel->hide();
|
||||||
|
|
||||||
|
headerLayout->addWidget(m_symbolLabel);
|
||||||
|
headerLayout->addWidget(m_nameLabel);
|
||||||
|
headerLayout->addStretch(1);
|
||||||
|
headerLayout->addWidget(m_slotDot);
|
||||||
|
headerLayout->addWidget(m_slotLabel);
|
||||||
|
cardLayout->addWidget(header);
|
||||||
|
|
||||||
|
m_configurationGroup = new QWidget(this);
|
||||||
|
QVBoxLayout* configurationLayout = new QVBoxLayout(m_configurationGroup);
|
||||||
|
configurationLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
configurationLayout->setSpacing(kCardSpacingPx);
|
||||||
|
cardLayout->addWidget(m_configurationGroup);
|
||||||
|
|
||||||
|
m_runtimeGroup = new QWidget(this);
|
||||||
|
QVBoxLayout* runtimeLayout = new QVBoxLayout(m_runtimeGroup);
|
||||||
|
runtimeLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
runtimeLayout->setSpacing(kCardSpacingPx);
|
||||||
|
cardLayout->addWidget(m_runtimeGroup);
|
||||||
|
|
||||||
|
if (m_siteId.has_value())
|
||||||
|
{
|
||||||
|
// A site has no buffers and runs no production cycle, so whatever the subclass
|
||||||
|
// puts into the runtime group is not shown at all; the construction section
|
||||||
|
// takes its place (REQ-BLD-SITE-CONFIG, REQ-UI-SELECTION-CARD). The subclass
|
||||||
|
// still fills the group -- it just never becomes visible.
|
||||||
|
m_runtimeGroup->hide();
|
||||||
|
m_constructionLabel = new QLabel(this);
|
||||||
|
cardLayout->addWidget(m_constructionLabel);
|
||||||
|
|
||||||
|
setSlot(QColor(), tr("constructing"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionContent::~SelectionContent() = default;
|
||||||
|
|
||||||
|
void SelectionContent::refresh()
|
||||||
|
{
|
||||||
|
refreshConfiguration();
|
||||||
|
if (m_siteId.has_value())
|
||||||
|
{
|
||||||
|
refreshConstruction();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshRuntime();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVBoxLayout* SelectionContent::getConfigurationLayout()
|
||||||
|
{
|
||||||
|
return static_cast<QVBoxLayout*>(m_configurationGroup->layout());
|
||||||
|
}
|
||||||
|
|
||||||
|
QVBoxLayout* SelectionContent::getRuntimeLayout()
|
||||||
|
{
|
||||||
|
return static_cast<QVBoxLayout*>(m_runtimeGroup->layout());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionContent::setIdentity(const QPixmap& symbol, const QString& name)
|
||||||
|
{
|
||||||
|
m_symbolLabel->setPixmap(symbol);
|
||||||
|
m_symbolLabel->setVisible(!symbol.isNull());
|
||||||
|
m_nameLabel->setText(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionContent::setBuildingIdentity(BuildingType type, const QString& name)
|
||||||
|
{
|
||||||
|
const std::string iconName = buildingTypeId(type);
|
||||||
|
setIdentity(m_context.buildingIcons->getChip(iconName, kSymbolSizePx), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionContent::setSlot(const QColor& dotColor, const QString& caption)
|
||||||
|
{
|
||||||
|
if (dotColor.isValid())
|
||||||
|
{
|
||||||
|
m_slotDot->setPixmap(
|
||||||
|
renderStatusDot(dotColor, m_context.visuals->statusLight.outline));
|
||||||
|
m_slotDot->show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_slotDot->hide();
|
||||||
|
}
|
||||||
|
m_slotLabel->setText(caption);
|
||||||
|
m_slotLabel->setVisible(!caption.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionContent::setCountSlot(int count)
|
||||||
|
{
|
||||||
|
setSlot(QColor(), tr("x%1").arg(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionContent::clearSlot()
|
||||||
|
{
|
||||||
|
setSlot(QColor(), QString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionContent::setProductionStatusSlot(const Building& building)
|
||||||
|
{
|
||||||
|
// The classification is the simulation's, so the panel and the world's status light
|
||||||
|
// can never disagree (REQ-UI-SELECTION-STATUS, REQ-UI-STATUS-LIGHT).
|
||||||
|
const std::optional<ProductionStatus> status =
|
||||||
|
getProductionStatus(*m_context.config, building);
|
||||||
|
if (!status.has_value())
|
||||||
|
{
|
||||||
|
clearSlot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusLightVisuals& colors = m_context.visuals->statusLight;
|
||||||
|
// The Salvage Bay has no recipe and no cycle: its two states say whether it is
|
||||||
|
// holding scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY).
|
||||||
|
const bool isSalvageBay = (building.type == BuildingType::SalvageBay);
|
||||||
|
switch (*status)
|
||||||
|
{
|
||||||
|
case ProductionStatus::Unconfigured:
|
||||||
|
setSlot(colors.grey, tr("no recipe"));
|
||||||
|
break;
|
||||||
|
case ProductionStatus::Producing:
|
||||||
|
setSlot(colors.green, isSalvageBay ? tr("holding scrap") : tr("producing"));
|
||||||
|
break;
|
||||||
|
case ProductionStatus::Starved:
|
||||||
|
setSlot(colors.red, isSalvageBay ? tr("empty") : tr("missing input"));
|
||||||
|
break;
|
||||||
|
case ProductionStatus::Blocked:
|
||||||
|
setSlot(colors.yellow, tr("output full"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectionContent::refreshConstruction()
|
||||||
|
{
|
||||||
|
const ConstructionSite* site =
|
||||||
|
findSite(m_context.sim->getFactoryState(), *m_siteId);
|
||||||
|
if (!site)
|
||||||
|
{
|
||||||
|
// The site finished or was removed under the card. SelectionPanel rebuilds on
|
||||||
|
// the same refresh, so this only has to avoid reading a dead site.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString progress;
|
||||||
|
if (site->completesAt == 0)
|
||||||
|
{
|
||||||
|
progress = tr("Queued");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const BuildingDef* def =
|
||||||
|
m_context.config->buildings.findBuildingDef(site->type);
|
||||||
|
if (def && def->constructionTimeSeconds > 0)
|
||||||
|
{
|
||||||
|
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
|
||||||
|
const Tick elapsed =
|
||||||
|
m_context.sim->getCurrentTick() - (site->completesAt - duration);
|
||||||
|
const int percent = static_cast<int>(
|
||||||
|
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
|
||||||
|
progress = tr("Construction: %1%").arg(percent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
progress = tr("Building...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_constructionLabel->setText(progress);
|
||||||
|
}
|
||||||
101
src/ui/selection/SelectionContent.h
Normal file
101
src/ui/selection/SelectionContent.h
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "SelectionContext.h"
|
||||||
|
|
||||||
|
struct Building;
|
||||||
|
class QLabel;
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
// One card of the selection panel: the content shown for a particular kind of selection
|
||||||
|
// (REQ-UI-SELECTION-CARD). Every content is this base plus the parts its constructor
|
||||||
|
// puts into the two groups; which content is shown for which selection is decided in
|
||||||
|
// SelectionContentFactory (REQ-UI-SELECTION-CONTENT).
|
||||||
|
//
|
||||||
|
// The card has three parts, top to bottom:
|
||||||
|
// * the header -- identity symbol, name, and one optional right slot (a status
|
||||||
|
// indicator, a ship's behavior, or an object count);
|
||||||
|
// * the configuration group -- controls that change how the object is set up;
|
||||||
|
// * the runtime group -- what the object is currently doing.
|
||||||
|
//
|
||||||
|
// A construction site keeps its configuration group and has its whole runtime group
|
||||||
|
// replaced by the construction section (REQ-BLD-SITE-CONFIG). That rule lives here and
|
||||||
|
// nowhere else: a subclass fills both groups unconditionally in its constructor and
|
||||||
|
// never asks whether it is showing a site.
|
||||||
|
class SelectionContent : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
~SelectionContent() override;
|
||||||
|
|
||||||
|
// Re-reads the live values behind the card. It never changes the card's structure:
|
||||||
|
// a change that would (a site finishing, the selection changing) is a rebuild, and
|
||||||
|
// SelectionPanel owns that decision.
|
||||||
|
void refresh();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// constructionSiteId is set only while the card shows a construction site, in which
|
||||||
|
// case this base builds and drives the construction section in place of whatever the
|
||||||
|
// subclass puts into the runtime group.
|
||||||
|
SelectionContent(const SelectionContext& context,
|
||||||
|
std::optional<BuildingId> constructionSiteId,
|
||||||
|
QWidget* parent);
|
||||||
|
|
||||||
|
// Live values of the runtime group. Not called while the card shows a construction
|
||||||
|
// site, which has neither buffers nor a production cycle (REQ-BLD-SITE-CONFIG).
|
||||||
|
virtual void refreshRuntime() = 0;
|
||||||
|
|
||||||
|
// Live values of the configuration group. Called for a site too, because a site is
|
||||||
|
// configured exactly like the building it will become (REQ-BLD-SITE-CONFIG).
|
||||||
|
virtual void refreshConfiguration() {}
|
||||||
|
|
||||||
|
const SelectionContext& getContext() const { return m_context; }
|
||||||
|
|
||||||
|
// The two group layouts a subclass adds its parts to, in its constructor.
|
||||||
|
QVBoxLayout* getConfigurationLayout();
|
||||||
|
QVBoxLayout* getRuntimeLayout();
|
||||||
|
|
||||||
|
void setIdentity(const QPixmap& symbol, const QString& name);
|
||||||
|
// Header symbol for a building type: its chip icon, or nothing when the icon file is
|
||||||
|
// missing -- which is not an error (REQ-UI-BUILD-ICON, REQ-UI-SELECTION-CARD).
|
||||||
|
void setBuildingIdentity(BuildingType type, const QString& name);
|
||||||
|
|
||||||
|
// Fills the header's right slot. A null dot color leaves the dot off, for the slots
|
||||||
|
// that are a plain caption (a construction site, a ship's behavior).
|
||||||
|
void setSlot(const QColor& dotColor, const QString& caption);
|
||||||
|
// "x<count>", for an aggregated multi-selection (REQ-UI-SELECTION-AGGREGATE).
|
||||||
|
void setCountSlot(int count);
|
||||||
|
void clearSlot();
|
||||||
|
|
||||||
|
// Fills the right slot from the building's production status, mapped to the same
|
||||||
|
// colors and states the world's status light uses (REQ-UI-SELECTION-STATUS). Leaves
|
||||||
|
// the slot empty for a type that has no status light.
|
||||||
|
void setProductionStatusSlot(const Building& building);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void refreshConstruction();
|
||||||
|
|
||||||
|
SelectionContext m_context;
|
||||||
|
// Set while this card shows a construction site rather than a finished object.
|
||||||
|
std::optional<BuildingId> m_siteId;
|
||||||
|
|
||||||
|
QLabel* m_symbolLabel;
|
||||||
|
QLabel* m_nameLabel;
|
||||||
|
QLabel* m_slotDot;
|
||||||
|
QLabel* m_slotLabel;
|
||||||
|
|
||||||
|
QWidget* m_configurationGroup;
|
||||||
|
QWidget* m_runtimeGroup;
|
||||||
|
// Replaces the runtime group while this card shows a construction site; null
|
||||||
|
// otherwise.
|
||||||
|
QLabel* m_constructionLabel;
|
||||||
|
};
|
||||||
187
src/ui/selection/SelectionContentFactory.cpp
Normal file
187
src/ui/selection/SelectionContentFactory.cpp
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
#include "AutoProductionContent.h"
|
||||||
|
#include "BeltContent.h"
|
||||||
|
#include "DebrisContent.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "FieldMultiContent.h"
|
||||||
|
#include "HqContent.h"
|
||||||
|
#include "MultiBuildingContent.h"
|
||||||
|
#include "RecipeProductionContent.h"
|
||||||
|
#include "ShipContent.h"
|
||||||
|
#include "ShipIdentityComponent.h"
|
||||||
|
#include "ShipyardContent.h"
|
||||||
|
#include "SplitterContent.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
|
#include "StationContent.h"
|
||||||
|
#include "StorageContent.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// The catalog row a single building type belongs to (REQ-UI-SELECTION-CONTENT).
|
||||||
|
SelectionContentKind getKindForType(BuildingType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case BuildingType::Miner:
|
||||||
|
case BuildingType::Assembler:
|
||||||
|
return SelectionContentKind::RecipeProduction;
|
||||||
|
case BuildingType::Smelter:
|
||||||
|
case BuildingType::ReprocessingPlant:
|
||||||
|
return SelectionContentKind::AutoProduction;
|
||||||
|
case BuildingType::Shipyard:
|
||||||
|
return SelectionContentKind::Shipyard;
|
||||||
|
case BuildingType::SalvageBay:
|
||||||
|
return SelectionContentKind::Storage;
|
||||||
|
case BuildingType::Hq:
|
||||||
|
return SelectionContentKind::Hq;
|
||||||
|
case BuildingType::Belt:
|
||||||
|
case BuildingType::TunnelEntry:
|
||||||
|
case BuildingType::TunnelExit:
|
||||||
|
return SelectionContentKind::Belt;
|
||||||
|
case BuildingType::Splitter:
|
||||||
|
return SelectionContentKind::Splitter;
|
||||||
|
case BuildingType::PlayerDefenceStation:
|
||||||
|
case BuildingType::EnemyDefenceStation:
|
||||||
|
// Defence stations are field objects backed by entities; these two enum
|
||||||
|
// values exist only for cost and visuals lookup and never reach the panel as
|
||||||
|
// a building selection. The count summary is the harmless fallback.
|
||||||
|
return SelectionContentKind::MultiBuilding;
|
||||||
|
}
|
||||||
|
return SelectionContentKind::MultiBuilding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A belt, tunnel entry or tunnel exit -- the types whose card is the clear action alone
|
||||||
|
// and therefore aggregates (REQ-UI-SELECTION-AGGREGATE). The splitter is deliberately
|
||||||
|
// excluded: it carries per-object output filters, which have no aggregate.
|
||||||
|
bool isAggregatableBeltType(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Belt
|
||||||
|
|| type == BuildingType::TunnelEntry
|
||||||
|
|| type == BuildingType::TunnelExit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentKey chooseBuildingContent(const SelectionRequest& request, Simulation& sim)
|
||||||
|
{
|
||||||
|
const FactoryState& state = sim.getFactoryState();
|
||||||
|
|
||||||
|
if (request.buildings.size() == 1)
|
||||||
|
{
|
||||||
|
const BuildingId id = request.buildings.front();
|
||||||
|
const Building* building = findBuilding(state, id);
|
||||||
|
if (building)
|
||||||
|
{
|
||||||
|
return { getKindForType(building->type), false };
|
||||||
|
}
|
||||||
|
const ConstructionSite* site = findSite(state, id);
|
||||||
|
if (site)
|
||||||
|
{
|
||||||
|
return { getKindForType(site->type), true };
|
||||||
|
}
|
||||||
|
// The building went away under the panel; the selection is stale and there is
|
||||||
|
// nothing to show (REQ-UI-EMPTY-SELECTION).
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Several buildings aggregate into one card only when every part of that card
|
||||||
|
// aggregates (REQ-UI-SELECTION-AGGREGATE). Construction sites are excluded from the
|
||||||
|
// belt case: a site's card is its own construction progress, which several sites
|
||||||
|
// cannot share.
|
||||||
|
bool allAggregatableBelts = true;
|
||||||
|
for (BuildingId id : request.buildings)
|
||||||
|
{
|
||||||
|
const Building* building = findBuilding(state, id);
|
||||||
|
if (!building || !isAggregatableBeltType(building->type))
|
||||||
|
{
|
||||||
|
allAggregatableBelts = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allAggregatableBelts)
|
||||||
|
{
|
||||||
|
return { SelectionContentKind::Belt, false };
|
||||||
|
}
|
||||||
|
return { SelectionContentKind::MultiBuilding, false };
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentKey chooseFieldContent(const SelectionRequest& request, Simulation& sim)
|
||||||
|
{
|
||||||
|
// A full single-object card is shown for a lone actor, and for debris whether one
|
||||||
|
// piece or several -- debris is the field category's other aggregating content
|
||||||
|
// (REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-SELECTION-AGGREGATE).
|
||||||
|
if (request.actors.size() == 1 && request.debris.empty())
|
||||||
|
{
|
||||||
|
EntityAdmin& admin = sim.getAdmin();
|
||||||
|
const entt::entity actor = request.actors.front();
|
||||||
|
if (admin.isValid(actor) && admin.hasAll<ShipIdentityComponent>(actor))
|
||||||
|
{
|
||||||
|
return { SelectionContentKind::Ship, false };
|
||||||
|
}
|
||||||
|
if (admin.isValid(actor) && admin.hasAll<StationBodyComponent>(actor))
|
||||||
|
{
|
||||||
|
return { SelectionContentKind::Station, false };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.actors.empty() && !request.debris.empty())
|
||||||
|
{
|
||||||
|
return { SelectionContentKind::Debris, false };
|
||||||
|
}
|
||||||
|
return { SelectionContentKind::FieldMulti, false };
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
ContentKey chooseContent(const SelectionRequest& request, Simulation& sim)
|
||||||
|
{
|
||||||
|
if (request.isEmpty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// Buildings win, so a non-empty building selection is the whole selection
|
||||||
|
// (REQ-UI-SELECTION-CATEGORIES).
|
||||||
|
if (!request.buildings.empty())
|
||||||
|
{
|
||||||
|
return chooseBuildingContent(request, sim);
|
||||||
|
}
|
||||||
|
return chooseFieldContent(request, sim);
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectionContent* createContent(const ContentKey& key, const SelectionRequest& request,
|
||||||
|
const SelectionContext& context, QWidget* parent)
|
||||||
|
{
|
||||||
|
switch (key.kind)
|
||||||
|
{
|
||||||
|
case SelectionContentKind::None:
|
||||||
|
return nullptr;
|
||||||
|
case SelectionContentKind::RecipeProduction:
|
||||||
|
return new RecipeProductionContent(context, request, parent);
|
||||||
|
case SelectionContentKind::AutoProduction:
|
||||||
|
return new AutoProductionContent(context, request, parent);
|
||||||
|
case SelectionContentKind::Shipyard:
|
||||||
|
return new ShipyardContent(context, request, parent);
|
||||||
|
case SelectionContentKind::Storage:
|
||||||
|
return new StorageContent(context, request, parent);
|
||||||
|
case SelectionContentKind::Hq:
|
||||||
|
return new HqContent(context, request, parent);
|
||||||
|
case SelectionContentKind::Belt:
|
||||||
|
return new BeltContent(context, request, parent);
|
||||||
|
case SelectionContentKind::Splitter:
|
||||||
|
return new SplitterContent(context, request, parent);
|
||||||
|
case SelectionContentKind::MultiBuilding:
|
||||||
|
return new MultiBuildingContent(context, request, parent);
|
||||||
|
case SelectionContentKind::Ship:
|
||||||
|
return new ShipContent(context, request, parent);
|
||||||
|
case SelectionContentKind::Station:
|
||||||
|
return new StationContent(context, request, parent);
|
||||||
|
case SelectionContentKind::Debris:
|
||||||
|
return new DebrisContent(context, request, parent);
|
||||||
|
case SelectionContentKind::FieldMulti:
|
||||||
|
return new FieldMultiContent(context, request, parent);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
70
src/ui/selection/SelectionContentFactory.h
Normal file
70
src/ui/selection/SelectionContentFactory.h
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "SelectionContext.h"
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
class SelectionContent;
|
||||||
|
class Simulation;
|
||||||
|
|
||||||
|
// What is currently selected, in the selection panel's terms. The two categories are
|
||||||
|
// mutually exclusive (REQ-UI-SELECTION-CATEGORIES): either buildings is non-empty, or
|
||||||
|
// actors and/or debris are, never both.
|
||||||
|
struct SelectionRequest
|
||||||
|
{
|
||||||
|
std::vector<BuildingId> buildings;
|
||||||
|
std::vector<entt::entity> actors;
|
||||||
|
std::vector<entt::entity> debris;
|
||||||
|
|
||||||
|
bool isEmpty() const
|
||||||
|
{
|
||||||
|
return buildings.empty() && actors.empty() && debris.empty();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// One entry of the content catalog (REQ-UI-SELECTION-CONTENT). Selections that share an
|
||||||
|
// entry get the same content and differ only in the name and symbol in the header.
|
||||||
|
enum class SelectionContentKind
|
||||||
|
{
|
||||||
|
None, // nothing selected: the panel hides itself entirely
|
||||||
|
RecipeProduction, // Miner, Assembler
|
||||||
|
AutoProduction, // Smelter, Reprocessing Plant
|
||||||
|
Shipyard,
|
||||||
|
Storage, // Salvage Bay
|
||||||
|
Hq,
|
||||||
|
Belt, // Belt, Tunnel Entry, Tunnel Exit
|
||||||
|
Splitter,
|
||||||
|
MultiBuilding,
|
||||||
|
Ship,
|
||||||
|
Station,
|
||||||
|
Debris,
|
||||||
|
FieldMulti,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Identifies the card on screen. The panel rebuilds when this changes and only refreshes
|
||||||
|
// otherwise, so a construction site finishing swaps the card while a progress counter
|
||||||
|
// running does not.
|
||||||
|
struct ContentKey
|
||||||
|
{
|
||||||
|
SelectionContentKind kind = SelectionContentKind::None;
|
||||||
|
bool isSite = false;
|
||||||
|
|
||||||
|
bool operator==(const ContentKey& other) const
|
||||||
|
{
|
||||||
|
return kind == other.kind && isSite == other.isSite;
|
||||||
|
}
|
||||||
|
bool operator!=(const ContentKey& other) const { return !(*this == other); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// The card this selection calls for. This is where REQ-UI-SELECTION-AGGREGATE is
|
||||||
|
// decided: a multi-selection that aggregates resolves to the single-object kind, and
|
||||||
|
// everything else to one of the two count summaries.
|
||||||
|
ContentKey chooseContent(const SelectionRequest& request, Simulation& sim);
|
||||||
|
|
||||||
|
// Builds the card. Returns nullptr for SelectionContentKind::None.
|
||||||
|
SelectionContent* createContent(const ContentKey& key, const SelectionRequest& request,
|
||||||
|
const SelectionContext& context, QWidget* parent);
|
||||||
29
src/ui/selection/SelectionContext.h
Normal file
29
src/ui/selection/SelectionContext.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
struct GameConfig;
|
||||||
|
struct VisualsConfig;
|
||||||
|
class BuildingIconCache;
|
||||||
|
class ItemIconCache;
|
||||||
|
class Simulation;
|
||||||
|
|
||||||
|
// Everything a selection panel content reads that is not the selection itself: the
|
||||||
|
// simulation it queries live values from, the immutable config, and the window-wide
|
||||||
|
// rendering resources. Bundled so a content's constructor stays short and adding a
|
||||||
|
// shared resource does not touch every content (REQ-UI-SELECTION-CONTENT).
|
||||||
|
//
|
||||||
|
// Nothing here is owned: the whole struct is a view onto objects living in MainWindow
|
||||||
|
// and must not outlive it. Passed by const reference and copied into each content.
|
||||||
|
struct SelectionContext
|
||||||
|
{
|
||||||
|
Simulation* sim = nullptr;
|
||||||
|
const GameConfig* config = nullptr;
|
||||||
|
const VisualsConfig* visuals = nullptr;
|
||||||
|
ItemIconCache* itemIcons = nullptr;
|
||||||
|
BuildingIconCache* buildingIcons = nullptr;
|
||||||
|
|
||||||
|
// Whether debug draw is currently on (REQ-UI-DEBUG-DRAW), which the ship card shows
|
||||||
|
// its threat cost under (REQ-UI-SHIP-STATS-PANEL). A pointer rather than a copy
|
||||||
|
// because cards outlive a toggle: the panel owns the flag, so a card reading it per
|
||||||
|
// refresh always sees the current value without subscribing to the event itself.
|
||||||
|
const bool* debugDrawEnabled = nullptr;
|
||||||
|
};
|
||||||
16
src/ui/selection/SelectionNames.cpp
Normal file
16
src/ui/selection/SelectionNames.cpp
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#include "SelectionNames.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "DisplayName.h"
|
||||||
|
|
||||||
|
QString getBuildingTypeName(BuildingType type)
|
||||||
|
{
|
||||||
|
// "Hq" would read as a word rather than an acronym through the generic conversion,
|
||||||
|
// and the player's own base deserves naming as such.
|
||||||
|
if (type == BuildingType::Hq)
|
||||||
|
{
|
||||||
|
return QObject::tr("Player HQ");
|
||||||
|
}
|
||||||
|
return QString::fromStdString(toDisplayName(buildingTypeId(type)));
|
||||||
|
}
|
||||||
10
src/ui/selection/SelectionNames.h
Normal file
10
src/ui/selection/SelectionNames.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
|
||||||
|
// Display name of a building type for the selection panel's header and count rows
|
||||||
|
// (REQ-UI-SELECTION-CARD, REQ-UI-MULTI-SELECTION). The name is derived from the type's
|
||||||
|
// config id, so a new building type needs no entry here.
|
||||||
|
QString getBuildingTypeName(BuildingType type);
|
||||||
62
src/ui/selection/ShipContent.cpp
Normal file
62
src/ui/selection/ShipContent.cpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#include "ShipContent.h"
|
||||||
|
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "EntityAdmin.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "HealthComponent.h"
|
||||||
|
#include "SelectedBehaviorComponent.h"
|
||||||
|
#include "ShipIdentityComponent.h"
|
||||||
|
#include "ShipStatsCalculator.h"
|
||||||
|
#include "ShipStatsPanel.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "ThreatCostCalculator.h"
|
||||||
|
|
||||||
|
ShipContent::ShipContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent)
|
||||||
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
|
, m_entity(request.actors.front())
|
||||||
|
{
|
||||||
|
m_statsPanel = new ShipStatsPanel(context.config, this);
|
||||||
|
getRuntimeLayout()->addWidget(m_statsPanel);
|
||||||
|
|
||||||
|
EntityAdmin& admin = context.sim->getAdmin();
|
||||||
|
if (admin.isValid(m_entity) && admin.hasAll<ShipIdentityComponent>(m_entity))
|
||||||
|
{
|
||||||
|
setIdentity(QPixmap(), tr("Ship: %1").arg(QString::fromStdString(
|
||||||
|
admin.get<ShipIdentityComponent>(m_entity).schematicId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipContent::refreshRuntime()
|
||||||
|
{
|
||||||
|
EntityAdmin& admin = getContext().sim->getAdmin();
|
||||||
|
if (!admin.isValid(m_entity) || !admin.hasAll<HealthComponent>(m_entity))
|
||||||
|
{
|
||||||
|
// The ship died or despawned. GameWorldView prunes it from the selection and
|
||||||
|
// re-emits (REQ-UI-ENTITY-CLICK-SELECT), which rebuilds the card; this only has
|
||||||
|
// to avoid reading it in the meantime.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HealthComponent& health = admin.get<HealthComponent>(m_entity);
|
||||||
|
if (health.hp <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShipStats stats = buildShipStatsFromEntity(admin, m_entity);
|
||||||
|
m_statsPanel->refreshFromLive(stats, health.hp);
|
||||||
|
m_statsPanel->setBehavior(admin.get<SelectedBehaviorComponent>(m_entity).winner);
|
||||||
|
m_statsPanel->setDebugDrawEnabled(*getContext().debugDrawEnabled);
|
||||||
|
|
||||||
|
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(m_entity);
|
||||||
|
const ShipDef* schematicDef =
|
||||||
|
getContext().config->ships.findShipDef(identity.schematicId);
|
||||||
|
if (schematicDef)
|
||||||
|
{
|
||||||
|
m_statsPanel->setThreatCost(calculateShipThreatCost(
|
||||||
|
getContext().config->threatCosts, *getContext().config,
|
||||||
|
schematicDef->id, schematicDef->defaultModules));
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/ui/selection/ShipContent.h
Normal file
28
src/ui/selection/ShipContent.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "SelectionContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
class ShipStatsPanel;
|
||||||
|
|
||||||
|
// The card for one selected ship (REQ-UI-SHIP-STATS-PANEL): its live hull stats and the
|
||||||
|
// summaries of the capability modules it carries, computed from what is actually
|
||||||
|
// installed (REQ-MOD-STAT-CALC). Applies to player and enemy ships alike
|
||||||
|
// (REQ-UI-ENTITY-CLICK-SELECT).
|
||||||
|
class ShipContent : public SelectionContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
ShipContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshRuntime() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
entt::entity m_entity;
|
||||||
|
ShipStatsPanel* m_statsPanel;
|
||||||
|
};
|
||||||
103
src/ui/selection/ShipyardContent.cpp
Normal file
103
src/ui/selection/ShipyardContent.cpp
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#include "ShipyardContent.h"
|
||||||
|
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingTarget.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "LayoutDialogRequestedEvent.h"
|
||||||
|
#include "ProductionRules.h"
|
||||||
|
#include "RecipeSelectionControl.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
|
#include "ShipLayoutPreview.h"
|
||||||
|
|
||||||
|
ShipyardContent::ShipyardContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent)
|
||||||
|
: BufferedBuildingContent(context, request.buildings.front(), parent)
|
||||||
|
{
|
||||||
|
m_schematicControl = new RecipeSelectionControl(context, getBuildingId(),
|
||||||
|
BuildingType::Shipyard, this);
|
||||||
|
m_layoutPreview = new ShipLayoutPreview(this);
|
||||||
|
m_configureButton = new QPushButton(tr("Configure Layout"), this);
|
||||||
|
|
||||||
|
getConfigurationLayout()->addWidget(m_schematicControl);
|
||||||
|
getConfigurationLayout()->addWidget(m_layoutPreview);
|
||||||
|
getConfigurationLayout()->addWidget(m_configureButton);
|
||||||
|
|
||||||
|
const BuildingId id = getBuildingId();
|
||||||
|
connect(m_configureButton, &QPushButton::clicked, this, [id]() {
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<LayoutDialogRequestedEvent>(id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShipyardContent::refreshConfiguration()
|
||||||
|
{
|
||||||
|
const BuildingTarget target = resolveBuildingTarget(getContext(), getBuildingId());
|
||||||
|
if (!target.isValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
||||||
|
m_schematicControl->setRecipeId(target.recipeId);
|
||||||
|
|
||||||
|
// The preview and Configure button are always shown for a shipyard and are only
|
||||||
|
// enabled once a schematic is selected (REQ-MOD-UI-PREVIEW). The schematic arrives
|
||||||
|
// by queued command, so this refresh is what picks it up rather than the click that
|
||||||
|
// chose it.
|
||||||
|
const ShipDef* shipDef = target.recipeId.empty()
|
||||||
|
? nullptr
|
||||||
|
: getContext().config->ships.findShipDef(target.recipeId);
|
||||||
|
const bool hasSchematic = shipDef && !shipDef->layout.empty();
|
||||||
|
if (hasSchematic)
|
||||||
|
{
|
||||||
|
m_layoutPreview->setShipAndLayout(
|
||||||
|
shipDef->layout,
|
||||||
|
target.shipLayout.has_value() ? *target.shipLayout : ShipLayoutConfig(),
|
||||||
|
&getContext().config->modules);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_layoutPreview->showPlaceholder();
|
||||||
|
}
|
||||||
|
m_layoutPreview->setEnabled(hasSchematic);
|
||||||
|
m_configureButton->setEnabled(hasSchematic);
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedBuildingContent::CycleInfo ShipyardContent::getCycleInfo(
|
||||||
|
const Building& building) const
|
||||||
|
{
|
||||||
|
CycleInfo info;
|
||||||
|
const ShipDef* shipDef = building.recipeId.empty()
|
||||||
|
? nullptr
|
||||||
|
: getContext().config->ships.findShipDef(building.recipeId);
|
||||||
|
if (!shipDef)
|
||||||
|
{
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The schematic's materials plus every placed module's, which is also what sized the
|
||||||
|
// input buffers (REQ-BLD-SHIPYARD). The simulation owns that sum, so the panel asks
|
||||||
|
// it rather than adding the modules up a second time.
|
||||||
|
info.perCycleInputs =
|
||||||
|
computeShipyardRequiredMaterials(*getContext().config, building);
|
||||||
|
|
||||||
|
info.durationSeconds = shipDef->schematic.productionTimeSeconds;
|
||||||
|
if (building.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& placed : building.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* moduleDef =
|
||||||
|
getContext().config->modules.findModuleDef(placed.moduleId);
|
||||||
|
if (moduleDef)
|
||||||
|
{
|
||||||
|
info.durationSeconds += moduleDef->productionTimeSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.runsProduction = true;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
32
src/ui/selection/ShipyardContent.h
Normal file
32
src/ui/selection/ShipyardContent.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BufferedBuildingContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
class QPushButton;
|
||||||
|
class RecipeSelectionControl;
|
||||||
|
class ShipLayoutPreview;
|
||||||
|
|
||||||
|
// The card for a Shipyard (REQ-UI-SELECTION-CONTENT): the schematic selection, the
|
||||||
|
// module layout preview and its Configure button (REQ-MOD-UI-PREVIEW), plus the buffers
|
||||||
|
// and production progress every buffered building shows.
|
||||||
|
//
|
||||||
|
// It is the one card whose cycle is not a recipe: a shipyard's materials and production
|
||||||
|
// time are its schematic's plus every placed module's (REQ-BLD-SHIPYARD).
|
||||||
|
class ShipyardContent : public BufferedBuildingContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
ShipyardContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshConfiguration() override;
|
||||||
|
CycleInfo getCycleInfo(const Building& building) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
RecipeSelectionControl* m_schematicControl;
|
||||||
|
ShipLayoutPreview* m_layoutPreview;
|
||||||
|
QPushButton* m_configureButton;
|
||||||
|
};
|
||||||
211
src/ui/selection/SplitterContent.cpp
Normal file
211
src/ui/selection/SplitterContent.cpp
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
#include "SplitterContent.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "BuildingTarget.h"
|
||||||
|
#include "ClearBeltControl.h"
|
||||||
|
#include "Command.h"
|
||||||
|
#include "CommandRequestedEvent.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Height cap on a filter list, so two of them plus the rest of the card still fit.
|
||||||
|
const int kFilterListHeightPx = 100;
|
||||||
|
|
||||||
|
QString getRotationLabel(Rotation rotation)
|
||||||
|
{
|
||||||
|
// Written as code points because the sources are read as ASCII by the compiler.
|
||||||
|
const QChar upArrow(0x2191); // U+2191 UPWARDS ARROW
|
||||||
|
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
||||||
|
const QChar downArrow(0x2193); // U+2193 DOWNWARDS ARROW
|
||||||
|
const QChar leftArrow(0x2190); // U+2190 LEFTWARDS ARROW
|
||||||
|
|
||||||
|
switch (rotation)
|
||||||
|
{
|
||||||
|
case Rotation::North: return QObject::tr("North (%1)").arg(upArrow);
|
||||||
|
case Rotation::East: return QObject::tr("East (%1)").arg(rightArrow);
|
||||||
|
case Rotation::South: return QObject::tr("South (%1)").arg(downArrow);
|
||||||
|
case Rotation::West: return QObject::tr("West (%1)").arg(leftArrow);
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every item type the economy knows, from both sides of every recipe.
|
||||||
|
std::vector<std::string> getAllItemIds(const RecipesConfig& recipes)
|
||||||
|
{
|
||||||
|
std::set<std::string> seen;
|
||||||
|
for (const RecipeDef& recipe : recipes.recipes)
|
||||||
|
{
|
||||||
|
for (const RecipeIngredient& ingredient : recipe.inputs)
|
||||||
|
{
|
||||||
|
seen.insert(ingredient.item);
|
||||||
|
}
|
||||||
|
for (const RecipeOutput& output : recipe.outputs)
|
||||||
|
{
|
||||||
|
seen.insert(output.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::vector<std::string>(seen.begin(), seen.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ItemType> collectCheckedItems(const QListWidget* list)
|
||||||
|
{
|
||||||
|
std::vector<ItemType> filter;
|
||||||
|
for (int row = 0; row < list->count(); ++row)
|
||||||
|
{
|
||||||
|
const QListWidgetItem* item = list->item(row);
|
||||||
|
if (item->checkState() == Qt::Checked)
|
||||||
|
{
|
||||||
|
filter.push_back(ItemType{ item->text().toStdString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
SplitterContent::SplitterContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent)
|
||||||
|
: SelectionContent(context, asConstructionSite(context, request.buildings.front()),
|
||||||
|
parent)
|
||||||
|
, m_id(request.buildings.front())
|
||||||
|
, m_isSite(asConstructionSite(context, m_id).has_value())
|
||||||
|
, m_tile(0, 0)
|
||||||
|
{
|
||||||
|
m_filterALabel = new QLabel(this);
|
||||||
|
m_filterAList = new QListWidget(this);
|
||||||
|
m_filterBLabel = new QLabel(this);
|
||||||
|
m_filterBList = new QListWidget(this);
|
||||||
|
m_filterAList->setMaximumHeight(kFilterListHeightPx);
|
||||||
|
m_filterBList->setMaximumHeight(kFilterListHeightPx);
|
||||||
|
|
||||||
|
getConfigurationLayout()->addWidget(m_filterALabel);
|
||||||
|
getConfigurationLayout()->addWidget(m_filterAList);
|
||||||
|
getConfigurationLayout()->addWidget(m_filterBLabel);
|
||||||
|
getConfigurationLayout()->addWidget(m_filterBList);
|
||||||
|
|
||||||
|
getRuntimeLayout()->addWidget(
|
||||||
|
new ClearBeltControl(context, request.buildings, this));
|
||||||
|
|
||||||
|
// Populated once, here, rather than on every refresh: re-checking the boxes at 30 Hz
|
||||||
|
// would fight the player's clicks.
|
||||||
|
populateFilters();
|
||||||
|
|
||||||
|
connect(m_filterAList, &QListWidget::itemChanged,
|
||||||
|
this, [this]() { applyFilters(); });
|
||||||
|
connect(m_filterBList, &QListWidget::itemChanged,
|
||||||
|
this, [this]() { applyFilters(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void SplitterContent::refreshConfiguration()
|
||||||
|
{
|
||||||
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
||||||
|
if (!target.isValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SplitterContent::populateFilters()
|
||||||
|
{
|
||||||
|
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
||||||
|
if (!target.isValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An operational splitter's outputs and filters live in the belt subsystem, keyed by
|
||||||
|
// tile; a site's are stored on the site itself (REQ-BLD-SITE-CONFIG).
|
||||||
|
std::optional<BeltSystem::SplitterInfo> info;
|
||||||
|
if (m_isSite)
|
||||||
|
{
|
||||||
|
info = getSiteSplitterInfo(getContext().sim->getFactoryState(),
|
||||||
|
*getContext().config, m_id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_tile = target.anchor;
|
||||||
|
info = getContext().sim->getBelts().getSplitterInfo(m_tile);
|
||||||
|
}
|
||||||
|
if (!info.has_value())
|
||||||
|
{
|
||||||
|
m_filterALabel->hide();
|
||||||
|
m_filterAList->hide();
|
||||||
|
m_filterBLabel->hide();
|
||||||
|
m_filterBList->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string> itemIds = getAllItemIds(getContext().config->recipes);
|
||||||
|
|
||||||
|
auto fillList = [&](QListWidget* list, QLabel* label, const QString& directionLabel,
|
||||||
|
const std::vector<ItemType>& filter)
|
||||||
|
{
|
||||||
|
label->setText(tr("%1 filter (empty = all):").arg(directionLabel));
|
||||||
|
list->blockSignals(true);
|
||||||
|
list->clear();
|
||||||
|
for (const std::string& itemId : itemIds)
|
||||||
|
{
|
||||||
|
// Only implicitly unlocked item types are offered (REQ-LOCK-UI-SPLITTER).
|
||||||
|
if (!getContext().sim->isItemUnlocked(itemId)) { continue; }
|
||||||
|
|
||||||
|
QListWidgetItem* row =
|
||||||
|
new QListWidgetItem(QString::fromStdString(itemId), list);
|
||||||
|
const bool checked = !filter.empty()
|
||||||
|
&& std::find(filter.begin(), filter.end(), ItemType{ itemId })
|
||||||
|
!= filter.end();
|
||||||
|
row->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
|
||||||
|
row->setFlags(row->flags() | Qt::ItemIsUserCheckable);
|
||||||
|
}
|
||||||
|
list->blockSignals(false);
|
||||||
|
label->show();
|
||||||
|
list->show();
|
||||||
|
};
|
||||||
|
|
||||||
|
fillList(m_filterAList, m_filterALabel, getRotationLabel(info->outputA),
|
||||||
|
info->filterA);
|
||||||
|
fillList(m_filterBList, m_filterBLabel, getRotationLabel(info->outputB),
|
||||||
|
info->filterB);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SplitterContent::applyFilters()
|
||||||
|
{
|
||||||
|
if (m_isSite)
|
||||||
|
{
|
||||||
|
std::shared_ptr<SetSiteSplitterFiltersCommand> command =
|
||||||
|
std::make_shared<SetSiteSplitterFiltersCommand>();
|
||||||
|
command->id = m_id;
|
||||||
|
command->filterA = collectCheckedItems(m_filterAList);
|
||||||
|
command->filterB = collectCheckedItems(m_filterBList);
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<CommandRequestedEvent>(command));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<SetSplitterFiltersCommand> command =
|
||||||
|
std::make_shared<SetSplitterFiltersCommand>();
|
||||||
|
command->tile = m_tile;
|
||||||
|
command->filterA = collectCheckedItems(m_filterAList);
|
||||||
|
command->filterB = collectCheckedItems(m_filterBList);
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<CommandRequestedEvent>(command));
|
||||||
|
}
|
||||||
47
src/ui/selection/SplitterContent.h
Normal file
47
src/ui/selection/SplitterContent.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "SelectionContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
class QListWidget;
|
||||||
|
|
||||||
|
// The card for a Splitter (REQ-UI-SELECTION-CONTENT): its two per-output item filters
|
||||||
|
// (REQ-BLD-SPLITTER) plus the clear action every belt-subsystem tile has
|
||||||
|
// (REQ-UI-BELT-CLEAR).
|
||||||
|
//
|
||||||
|
// The filters are configuration, so they are shown for a construction site too and set
|
||||||
|
// through the site's own command (REQ-BLD-SITE-CONFIG); the clear action is runtime and
|
||||||
|
// therefore is not, because a site's tile is not registered with the belt subsystem yet.
|
||||||
|
class SplitterContent : public SelectionContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
SplitterContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshConfiguration() override;
|
||||||
|
void refreshRuntime() override {}
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Sends the checked items of both lists as the splitter's new filters. Routed to the
|
||||||
|
// site command or the live command depending on what the id names.
|
||||||
|
void applyFilters();
|
||||||
|
// Rebuilds both lists from the splitter's current outputs and filters. Only run when
|
||||||
|
// the lists are not already populated, so a per-tick refresh cannot uncheck a box
|
||||||
|
// the player is in the middle of clicking.
|
||||||
|
void populateFilters();
|
||||||
|
|
||||||
|
BuildingId m_id;
|
||||||
|
bool m_isSite;
|
||||||
|
QPoint m_tile;
|
||||||
|
QLabel* m_filterALabel;
|
||||||
|
QListWidget* m_filterAList;
|
||||||
|
QLabel* m_filterBLabel;
|
||||||
|
QListWidget* m_filterBList;
|
||||||
|
};
|
||||||
66
src/ui/selection/StationContent.cpp
Normal file
66
src/ui/selection/StationContent.cpp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#include "StationContent.h"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "EntityAdmin.h"
|
||||||
|
#include "FactionComponent.h"
|
||||||
|
#include "HealthComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "WeaponComponent.h"
|
||||||
|
|
||||||
|
StationContent::StationContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent)
|
||||||
|
: SelectionContent(context, std::nullopt, parent)
|
||||||
|
, m_entity(request.actors.front())
|
||||||
|
{
|
||||||
|
m_statsLabel = new QLabel(this);
|
||||||
|
m_statsLabel->setWordWrap(true);
|
||||||
|
getRuntimeLayout()->addWidget(m_statsLabel);
|
||||||
|
|
||||||
|
EntityAdmin& admin = context.sim->getAdmin();
|
||||||
|
const bool isEnemy = admin.isValid(m_entity)
|
||||||
|
&& admin.hasAll<FactionComponent>(m_entity)
|
||||||
|
&& admin.get<FactionComponent>(m_entity).isEnemy;
|
||||||
|
setIdentity(QPixmap(), isEnemy ? tr("Enemy Defence Station")
|
||||||
|
: tr("Player Defence Station"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void StationContent::refreshRuntime()
|
||||||
|
{
|
||||||
|
EntityAdmin& admin = getContext().sim->getAdmin();
|
||||||
|
if (!admin.isValid(m_entity) || !admin.hasAll<HealthComponent>(m_entity))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const HealthComponent& health = admin.get<HealthComponent>(m_entity);
|
||||||
|
|
||||||
|
// A station's weapons are child module entities pointing back at it, so its combined
|
||||||
|
// damage and range are summed over those rather than read off the station itself.
|
||||||
|
float totalDps = 0.0f;
|
||||||
|
float maxRange = 0.0f;
|
||||||
|
bool hasWeapon = false;
|
||||||
|
const entt::entity station = m_entity;
|
||||||
|
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
||||||
|
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner,
|
||||||
|
const WeaponComponent& weapon)
|
||||||
|
{
|
||||||
|
if (owner.owner != station) { return; }
|
||||||
|
hasWeapon = true;
|
||||||
|
totalDps += weapon.damage * weapon.fireRateHz;
|
||||||
|
if (weapon.range_tiles > maxRange) { maxRange = weapon.range_tiles; }
|
||||||
|
});
|
||||||
|
|
||||||
|
QString text = tr("HP: %1 / %2")
|
||||||
|
.arg(static_cast<int>(health.hp + 0.5f))
|
||||||
|
.arg(static_cast<int>(health.maxHp + 0.5f));
|
||||||
|
if (hasWeapon)
|
||||||
|
{
|
||||||
|
text += tr("\nDPS: %1")
|
||||||
|
.arg(QString::number(static_cast<double>(totalDps), 'f', 1));
|
||||||
|
text += tr("\nRange: %1 tiles")
|
||||||
|
.arg(QString::number(static_cast<double>(maxRange), 'f', 1));
|
||||||
|
}
|
||||||
|
m_statsLabel->setText(text);
|
||||||
|
}
|
||||||
27
src/ui/selection/StationContent.h
Normal file
27
src/ui/selection/StationContent.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "SelectionContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
// The card for one selected defence station, player or enemy
|
||||||
|
// (REQ-UI-STATION-STATS-PANEL): its HP plus the combined damage, range and fire rate of
|
||||||
|
// the weapon modules mounted on it.
|
||||||
|
class StationContent : public SelectionContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
StationContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshRuntime() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
entt::entity m_entity;
|
||||||
|
QLabel* m_statsLabel;
|
||||||
|
};
|
||||||
29
src/ui/selection/StorageContent.cpp
Normal file
29
src/ui/selection/StorageContent.cpp
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#include "StorageContent.h"
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingTarget.h"
|
||||||
|
#include "SelectionNames.h"
|
||||||
|
|
||||||
|
StorageContent::StorageContent(const SelectionContext& context,
|
||||||
|
const SelectionRequest& request, QWidget* parent)
|
||||||
|
: BufferedBuildingContent(context, request.buildings.front(), parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void StorageContent::refreshConfiguration()
|
||||||
|
{
|
||||||
|
const BuildingTarget target = resolveBuildingTarget(getContext(), getBuildingId());
|
||||||
|
if (!target.isValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedBuildingContent::CycleInfo StorageContent::getCycleInfo(
|
||||||
|
const Building& /*building*/) const
|
||||||
|
{
|
||||||
|
// No recipe, no cycle: the card shows the output buffer alone, with no per-cycle
|
||||||
|
// denominators to show it against (REQ-BLD-SALVAGE-BAY).
|
||||||
|
return CycleInfo();
|
||||||
|
}
|
||||||
22
src/ui/selection/StorageContent.h
Normal file
22
src/ui/selection/StorageContent.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BufferedBuildingContent.h"
|
||||||
|
#include "SelectionContentFactory.h"
|
||||||
|
|
||||||
|
// The card for a Salvage Bay (REQ-UI-SELECTION-CONTENT): its held scrap and nothing
|
||||||
|
// else. It has no recipe to configure and runs no production cycle
|
||||||
|
// (REQ-BLD-SALVAGE-BAY), so it is the buffered-building card with both of those left
|
||||||
|
// out -- its header status says whether it is holding scrap rather than whether it is
|
||||||
|
// producing (REQ-UI-SELECTION-STATUS).
|
||||||
|
class StorageContent : public BufferedBuildingContent
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
StorageContent(const SelectionContext& context, const SelectionRequest& request,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void refreshConfiguration() override;
|
||||||
|
CycleInfo getCycleInfo(const Building& building) const override;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user