Compare commits
2 Commits
90e40dddbc
...
246cfc3935
| Author | SHA1 | Date | |
|---|---|---|---|
| 246cfc3935 | |||
| 89e984ec76 |
@@ -125,6 +125,7 @@ Three product targets plus tests:
|
||||
|
||||
- `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/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`.
|
||||
- `tests/` — Catch2 tests. Links only against `lib`.
|
||||
|
||||
|
||||
@@ -10,7 +10,12 @@ set(TARGET_LIB_INCLUDE_DIRS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/lib"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/external"
|
||||
)
|
||||
set(TARGET_UI_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/ui")
|
||||
set(TARGET_UI_INCLUDE_DIRS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/ui"
|
||||
# The balancing target compiles a few ui files into itself rather than linking the
|
||||
# ui library, and the ship stats panel is built from the selection card's parts.
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/ui/selection"
|
||||
)
|
||||
set(TARGET_TEST_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/test")
|
||||
set(TARGET_BALANCING_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/balancing")
|
||||
|
||||
@@ -78,6 +83,7 @@ unset(SRCS)
|
||||
|
||||
set(HDRS)
|
||||
set(SRCS)
|
||||
set(UI_INCLUDE_PATH)
|
||||
|
||||
add_subdirectory(ui)
|
||||
|
||||
@@ -106,6 +112,7 @@ set_target_properties(${TARGET_UI_NAME} PROPERTIES
|
||||
)
|
||||
target_include_directories(${TARGET_UI_NAME} PUBLIC
|
||||
"${TARGET_UI_INCLUDE_DIRS}"
|
||||
"${UI_INCLUDE_PATH}"
|
||||
"${TARGET_LIB_INCLUDE_DIRS}"
|
||||
"${LIB_INCLUDE_PATH}"
|
||||
)
|
||||
|
||||
@@ -7,6 +7,12 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.h
|
||||
# The card parts the ship stats panel is built from. They are deliberately free of
|
||||
# Simulation and GameConfig, which is what lets them come along here.
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/StatRow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/BarRow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SectionBox.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SelectionNames.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.h
|
||||
# Shared world-space shapes so the arena keeps looking like the game
|
||||
@@ -26,6 +32,10 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BalancingWindow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/StatRow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/BarRow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SectionBox.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SelectionNames.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../ui/WorldPrimitives.cpp
|
||||
PARENT_SCOPE
|
||||
|
||||
@@ -45,9 +45,17 @@ bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe)
|
||||
}
|
||||
std::map<std::string, int>
|
||||
computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
|
||||
{
|
||||
return computeShipyardRequiredMaterials(config, b.recipeId, b.shipLayout);
|
||||
}
|
||||
|
||||
std::map<std::string, int>
|
||||
computeShipyardRequiredMaterials(const GameConfig& config,
|
||||
const std::string& recipeId,
|
||||
const std::optional<ShipLayoutConfig>& shipLayout)
|
||||
{
|
||||
std::map<std::string, int> requiredMaterials;
|
||||
const ShipDef* shipDef = config.ships.findShipDef(b.recipeId);
|
||||
const ShipDef* shipDef = config.ships.findShipDef(recipeId);
|
||||
if (!shipDef)
|
||||
{
|
||||
return requiredMaterials;
|
||||
@@ -56,9 +64,9 @@ computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
|
||||
{
|
||||
requiredMaterials[ing.item] += ing.amount;
|
||||
}
|
||||
if (b.shipLayout.has_value())
|
||||
if (shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||
for (const PlacedModule& pm : shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
|
||||
if (!modDef)
|
||||
|
||||
@@ -38,6 +38,12 @@ bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe);
|
||||
std::map<std::string, int> computeShipyardRequiredMaterials(const GameConfig& config,
|
||||
const Building& b);
|
||||
|
||||
// The same sum over a stored configuration rather than an operational building, so a
|
||||
// construction site's schematic can be costed before it is built (REQ-BLD-SITE-CONFIG).
|
||||
std::map<std::string, int> computeShipyardRequiredMaterials(
|
||||
const GameConfig& config, const std::string& recipeId,
|
||||
const std::optional<ShipLayoutConfig>& shipLayout);
|
||||
|
||||
// True when a production cycle could start right now, ignoring output-buffer space.
|
||||
bool hasInputsToStart(const GameConfig& config, const Building& b);
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QColor>
|
||||
#include <QFile>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QGuiApplication>
|
||||
@@ -15,12 +13,11 @@
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
#include <QRect>
|
||||
#include <QRegularExpression>
|
||||
#include <QSignalMapper>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QSvgRenderer>
|
||||
|
||||
#include "BuildingIconCache.h"
|
||||
#include "BuildingType.h"
|
||||
#include "BuildingTypeSelectedEvent.h"
|
||||
#include "DeconstructModeToggleRequestedEvent.h"
|
||||
@@ -54,45 +51,15 @@ namespace
|
||||
// Gap between the chip icon and the cost line on a button face.
|
||||
const int kFaceGapPx = 2;
|
||||
|
||||
// Rasterizes a chip SVG straight at its on-screen size times the device pixel
|
||||
// ratio, so it stays crisp without a downscale step.
|
||||
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
|
||||
// Normal and grey-background chip pixmaps for one icon name. Empty pixmaps if the
|
||||
// file cannot be read; the caller then falls back to a name caption
|
||||
// (REQ-UI-BUILD-ICON).
|
||||
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;
|
||||
result.normal = renderChip(svg);
|
||||
|
||||
// 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());
|
||||
result.normal = icons.getChip(iconName, kIconSize.width());
|
||||
result.grey = icons.getGreyChip(iconName, kIconSize.width());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -181,12 +148,12 @@ namespace
|
||||
|
||||
|
||||
BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
|
||||
const std::string& iconDir,
|
||||
BuildingIconCache* buildingIcons,
|
||||
ItemIconCache* itemIcons, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_sim(sim)
|
||||
, m_config(config)
|
||||
, m_iconDir(iconDir)
|
||||
, m_buildingIcons(buildingIcons)
|
||||
, m_itemIcons(itemIcons)
|
||||
{
|
||||
// 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)
|
||||
? tr("Tunnel")
|
||||
: QString::fromStdString(toDisplayName(def.id));
|
||||
|
||||
// Icon file name matches the building id (REQ-UI-BUILD-ICON); Tunnel Entry's
|
||||
// "tunnel_entry.svg" serves the shared Tunnel button.
|
||||
const QString iconPath = QString::fromStdString(m_iconDir) + "/"
|
||||
+ QString::fromStdString(def.id) + ".svg";
|
||||
|
||||
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());
|
||||
|
||||
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
|
||||
// (REQ-UI-DECONSTRUCT-BUTTON), and its Q toggle as the badge (REQ-UI-HOTKEYS).
|
||||
const ButtonFace deconstructFace = buildButtonFace(
|
||||
loadChipPixmaps(QString::fromStdString(m_iconDir) + "/deconstruct.svg"),
|
||||
loadChipPixmaps(*m_buildingIcons, "deconstruct"),
|
||||
QStringLiteral("Q"), tr("Deconstruct"), tr("Deconstruct"), QPixmap(),
|
||||
font(), palette());
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
class QPushButton;
|
||||
class Simulation;
|
||||
class BuildingIconCache;
|
||||
class ItemIconCache;
|
||||
|
||||
// The build menu: one horizontal row of build buttons floating over the game world
|
||||
@@ -36,13 +37,11 @@ class BuildButtonBar : public QWidget,
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// iconDir is the directory holding the per-building "<id>.svg" chip icons
|
||||
// (REQ-UI-BUILD-ICON), read from disk at runtime like the config files.
|
||||
// itemIcons is the window-wide per-item icon cache and supplies the
|
||||
// building_block icon shown in each button's cost (REQ-UI-BUILD-COST). Not
|
||||
// owned; must outlive this widget.
|
||||
// buildingIcons supplies each button's chip icon (REQ-UI-BUILD-ICON) and itemIcons
|
||||
// the building_block icon shown in each button's cost (REQ-UI-BUILD-COST). Both are
|
||||
// window-wide caches; neither is owned, and both must outlive this widget.
|
||||
BuildButtonBar(Simulation* sim, const GameConfig* config,
|
||||
const std::string& iconDir, ItemIconCache* itemIcons,
|
||||
BuildingIconCache* buildingIcons, ItemIconCache* itemIcons,
|
||||
QWidget* parent = nullptr);
|
||||
~BuildButtonBar() override;
|
||||
|
||||
@@ -85,8 +84,8 @@ private slots:
|
||||
private:
|
||||
Simulation* m_sim;
|
||||
const GameConfig* m_config;
|
||||
std::string m_iconDir;
|
||||
ItemIconCache* m_itemIcons; // Not owned; lives in MainWindow.
|
||||
BuildingIconCache* m_buildingIcons; // Not owned; lives in MainWindow.
|
||||
ItemIconCache* m_itemIcons; // Not owned; lives in MainWindow.
|
||||
std::vector<BuildingType> m_types;
|
||||
std::vector<QPushButton*> m_buttons;
|
||||
std::map<BuildingType, int> m_costs;
|
||||
|
||||
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
|
||||
${HDRS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsConfig.h
|
||||
@@ -12,7 +14,6 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||
@@ -22,10 +23,16 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
set(UI_INCLUDE_PATH
|
||||
${UI_INCLUDE_PATH}
|
||||
PARENT_SCOPE
|
||||
)
|
||||
|
||||
SET(SRCS
|
||||
${SRCS}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/VisualsLoader.cpp
|
||||
@@ -38,7 +45,6 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
||||
@@ -48,6 +54,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingIconCache.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.cpp
|
||||
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 "ShipLayoutBlueprintSerializer.h"
|
||||
#include "ShipLayoutDialog.h"
|
||||
#include "BuildingIconCache.h"
|
||||
#include "ItemIconCache.h"
|
||||
#include "ModalPauseScope.h"
|
||||
#include "Simulation.h"
|
||||
@@ -48,31 +49,28 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
||||
setWindowTitle(tr("Dota Factory"));
|
||||
resize(1280, 768);
|
||||
|
||||
// Item icons live alongside the config (a sibling of the config dir), read from
|
||||
// disk at runtime like the building icons and visuals.toml (REQ-UI-ITEM-ICON).
|
||||
const std::string itemsIconDir = QDir::cleanPath(
|
||||
QString::fromStdString(m_configDir) + "/../icons/items").toStdString();
|
||||
|
||||
// Item and building icons live alongside the config (siblings of the config dir),
|
||||
// read from disk at runtime the same way visuals.toml is (REQ-UI-ITEM-ICON,
|
||||
// REQ-UI-BUILD-ICON).
|
||||
const QString configDirPath = QString::fromStdString(m_configDir);
|
||||
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_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
|
||||
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
|
||||
// (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,
|
||||
// and before the dim overlay keeps modals dimming it too (REQ-UI-MODAL-DIM).
|
||||
// Its geometry comes from layoutPanels().
|
||||
m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(), iconDir,
|
||||
m_itemIcons.get(), this);
|
||||
m_buildButtonBar = new BuildButtonBar(sim, &sim->getConfig(),
|
||||
m_buildingIcons.get(), m_itemIcons.get(),
|
||||
this);
|
||||
|
||||
// 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
|
||||
@@ -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
|
||||
// what puts it above the world and its vignettes and below the dim overlay. It
|
||||
// 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
|
||||
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
|
||||
|
||||
@@ -31,6 +31,7 @@ class HeaderBar;
|
||||
class SelectionPanel;
|
||||
class BuildButtonBar;
|
||||
class BlueprintLibrary;
|
||||
class BuildingIconCache;
|
||||
class ItemIconCache;
|
||||
class QCloseEvent;
|
||||
class QResizeEvent;
|
||||
@@ -92,6 +93,9 @@ private:
|
||||
// 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.
|
||||
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;
|
||||
HeaderBar* m_headerBar;
|
||||
SelectionPanel* m_selectionPanel;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,48 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QPoint>
|
||||
#include <QRect>
|
||||
#include <QWidget>
|
||||
|
||||
#include "BeltSystem.h"
|
||||
#include "Building.h"
|
||||
#include "BuildingId.h"
|
||||
#include "DebrisSelectionChangedEvent.h"
|
||||
#include "DebugDrawToggledEvent.h"
|
||||
#include "EntitySelectionChangedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
#include "GameConfig.h"
|
||||
#include "PlayerCommandsAppliedEvent.h"
|
||||
#include "RecipesConfig.h"
|
||||
#include "DebrisSelectionChangedEvent.h"
|
||||
#include "SelectionChangedEvent.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipsConfig.h"
|
||||
#include "Tick.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 FieldSelectionPanel;
|
||||
class ShipLayoutPreview;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
class QScrollArea;
|
||||
class QVBoxLayout;
|
||||
|
||||
// Shows the current selection. The building category (buildings and construction sites)
|
||||
// is rendered by this panel itself; the field category (ships, defence stations, debris)
|
||||
// is rendered by the embedded FieldSelectionPanel.
|
||||
//
|
||||
// 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.
|
||||
// Shows the current selection. The panel itself renders nothing: it arbitrates between
|
||||
// the two selection categories (REQ-UI-SELECTION-CATEGORIES), picks the card that fits
|
||||
// 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 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
|
||||
// (REQ-UI-EMPTY-SELECTION).
|
||||
class SelectionPanel : public QWidget,
|
||||
@@ -50,13 +36,17 @@ class SelectionPanel : public QWidget,
|
||||
PlayerCommandsAppliedEvent,
|
||||
EntitySelectionChangedEvent,
|
||||
SelectionChangedEvent,
|
||||
DebrisSelectionChangedEvent>
|
||||
DebrisSelectionChangedEvent,
|
||||
DebugDrawToggledEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
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,
|
||||
QWidget* parent = nullptr);
|
||||
const VisualsConfig* visuals, ItemIconCache* itemIcons,
|
||||
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
||||
~SelectionPanel() override;
|
||||
|
||||
// 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);
|
||||
|
||||
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 PlayerCommandsAppliedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
||||
|
||||
private slots:
|
||||
void onSelectRecipeClicked();
|
||||
void onClearBelt();
|
||||
void onSplitterFilterChanged();
|
||||
|
||||
private:
|
||||
// Why the selection display is being refreshed. A periodic tick only needs a
|
||||
// lightweight content update (e.g. a construction site's progress label),
|
||||
// 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.
|
||||
// Re-reads the live values of the card on screen. When the selection now calls for a
|
||||
// different card -- a construction site finishing is the case that matters -- it
|
||||
// rebuilds instead, because a card never changes its own shape.
|
||||
void refreshContent();
|
||||
// Replaces the card with the one the current selection calls for.
|
||||
void rebuildContent();
|
||||
// Shows the panel while a card exists and hides it otherwise
|
||||
// (REQ-UI-EMPTY-SELECTION), re-fitting it to the card while it is shown.
|
||||
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 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;
|
||||
const GameConfig* m_config;
|
||||
std::vector<BuildingId> m_selectedBuildingIds;
|
||||
SelectionContext m_context;
|
||||
// Read through m_context by the cards that need it, so a toggle reaches the card on
|
||||
// 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
|
||||
// until the owner has anchored it for the first time.
|
||||
QRect m_bandRect;
|
||||
|
||||
// Scrolls the content once it outgrows the band (REQ-UI-SELECTION-PANEL). All the
|
||||
// content widgets below are children of m_content, not of the panel itself.
|
||||
// Scrolls the card once it outgrows the band (REQ-UI-SELECTION-PANEL). The card is a
|
||||
// child of m_body, not of the panel itself.
|
||||
QScrollArea* m_scrollArea;
|
||||
QWidget* m_content;
|
||||
|
||||
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;
|
||||
QWidget* m_body;
|
||||
QVBoxLayout* m_bodyLayout;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#include "ShipStatsPanel.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BarRow.h"
|
||||
#include "GameConfig.h"
|
||||
#include "SectionBox.h"
|
||||
#include "SelectionNames.h"
|
||||
#include "ShipStatsCalculator.h"
|
||||
#include "StatRow.h"
|
||||
#include "ThreatCostCalculator.h"
|
||||
|
||||
namespace
|
||||
@@ -16,20 +19,6 @@ QString fmt(float value)
|
||||
return QString::number(static_cast<double>(value), 'f', 1);
|
||||
}
|
||||
|
||||
QLabel* makeSectionHeader(const QString& text, QWidget* parent)
|
||||
{
|
||||
QLabel* label = new QLabel(text, parent);
|
||||
QFont f = label->font();
|
||||
f.setBold(true);
|
||||
label->setFont(f);
|
||||
return label;
|
||||
}
|
||||
|
||||
QLabel* makeStatLabel(QWidget* parent)
|
||||
{
|
||||
return new QLabel(parent);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -38,215 +27,150 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent)
|
||||
, m_config(config)
|
||||
{
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(4, 4, 4, 4);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(2);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
// Hull stats — always visible.
|
||||
m_hpLabel = makeStatLabel(this);
|
||||
m_speedLabel = makeStatLabel(this);
|
||||
m_sensorRangeLabel = makeStatLabel(this);
|
||||
m_mainAccelLabel = makeStatLabel(this);
|
||||
m_maneuveringAccelLabel = makeStatLabel(this);
|
||||
m_angularAccelLabel = makeStatLabel(this);
|
||||
m_maxRotSpeedLabel = makeStatLabel(this);
|
||||
m_cargoCapacityLabel = makeStatLabel(this);
|
||||
m_cargoCapacityLabel->setVisible(false);
|
||||
// Hull stats -- always visible, except cargo capacity, which only a ship that can
|
||||
// carry anything shows (REQ-MOD-UI-STATS-PANEL).
|
||||
m_hpBar = new BarRow(tr("HP"), this);
|
||||
m_speedRow = new StatRow(tr("Max speed"), this);
|
||||
m_sensorRangeRow = new StatRow(tr("Sensor range"), this);
|
||||
m_mainAccelRow = new StatRow(tr("Main accel"), this);
|
||||
m_maneuveringAccelRow = new StatRow(tr("Maneuvering accel"), this);
|
||||
m_angularAccelRow = new StatRow(tr("Angular accel"), this);
|
||||
m_maxRotSpeedRow = new StatRow(tr("Max rotation"), this);
|
||||
m_cargoCapacityRow = new StatRow(tr("Cargo capacity"), this);
|
||||
m_cargoCapacityRow->hide();
|
||||
|
||||
layout->addWidget(m_hpLabel);
|
||||
layout->addWidget(m_speedLabel);
|
||||
layout->addWidget(m_sensorRangeLabel);
|
||||
layout->addWidget(m_mainAccelLabel);
|
||||
layout->addWidget(m_maneuveringAccelLabel);
|
||||
layout->addWidget(m_angularAccelLabel);
|
||||
layout->addWidget(m_maxRotSpeedLabel);
|
||||
layout->addWidget(m_cargoCapacityLabel);
|
||||
layout->addWidget(m_hpBar);
|
||||
layout->addWidget(m_speedRow);
|
||||
layout->addWidget(m_sensorRangeRow);
|
||||
layout->addWidget(m_mainAccelRow);
|
||||
layout->addWidget(m_maneuveringAccelRow);
|
||||
layout->addWidget(m_angularAccelRow);
|
||||
layout->addWidget(m_maxRotSpeedRow);
|
||||
layout->addWidget(m_cargoCapacityRow);
|
||||
|
||||
// Weapon capability section.
|
||||
m_weaponSection = new QWidget(this);
|
||||
{
|
||||
QVBoxLayout* sl = new QVBoxLayout(m_weaponSection);
|
||||
sl->setContentsMargins(0, 4, 0, 0);
|
||||
sl->setSpacing(2);
|
||||
sl->addWidget(makeSectionHeader(tr("Weapons"), m_weaponSection));
|
||||
m_weaponDpsLabel = makeStatLabel(m_weaponSection);
|
||||
m_weaponRangeLabel = makeStatLabel(m_weaponSection);
|
||||
sl->addWidget(m_weaponDpsLabel);
|
||||
sl->addWidget(m_weaponRangeLabel);
|
||||
}
|
||||
m_weaponSection->setVisible(false);
|
||||
// One section per capability module type, each shown only while at least one such
|
||||
// module is installed (REQ-MOD-UI-STATS-PANEL).
|
||||
m_weaponSection = new SectionBox(tr("Weapons"), this);
|
||||
m_weaponDpsRow = new StatRow(tr("DPS"), m_weaponSection);
|
||||
m_weaponRangeRow = new StatRow(tr("Range"), m_weaponSection);
|
||||
m_weaponSection->getContentLayout()->addWidget(m_weaponDpsRow);
|
||||
m_weaponSection->getContentLayout()->addWidget(m_weaponRangeRow);
|
||||
m_weaponSection->hide();
|
||||
layout->addWidget(m_weaponSection);
|
||||
|
||||
// Salvage capability section.
|
||||
m_salvageSection = new QWidget(this);
|
||||
{
|
||||
QVBoxLayout* sl = new QVBoxLayout(m_salvageSection);
|
||||
sl->setContentsMargins(0, 4, 0, 0);
|
||||
sl->setSpacing(2);
|
||||
sl->addWidget(makeSectionHeader(tr("Salvage"), m_salvageSection));
|
||||
m_salvageRateLabel = makeStatLabel(m_salvageSection);
|
||||
m_salvageRangeLabel = makeStatLabel(m_salvageSection);
|
||||
sl->addWidget(m_salvageRateLabel);
|
||||
sl->addWidget(m_salvageRangeLabel);
|
||||
}
|
||||
m_salvageSection->setVisible(false);
|
||||
m_salvageSection = new SectionBox(tr("Salvage"), this);
|
||||
m_salvageRateRow = new StatRow(tr("Collection rate"), m_salvageSection);
|
||||
m_salvageRangeRow = new StatRow(tr("Range"), m_salvageSection);
|
||||
m_salvageSection->getContentLayout()->addWidget(m_salvageRateRow);
|
||||
m_salvageSection->getContentLayout()->addWidget(m_salvageRangeRow);
|
||||
m_salvageSection->hide();
|
||||
layout->addWidget(m_salvageSection);
|
||||
|
||||
// Repair capability section.
|
||||
m_repairSection = new QWidget(this);
|
||||
{
|
||||
QVBoxLayout* sl = new QVBoxLayout(m_repairSection);
|
||||
sl->setContentsMargins(0, 4, 0, 0);
|
||||
sl->setSpacing(2);
|
||||
sl->addWidget(makeSectionHeader(tr("Repair"), m_repairSection));
|
||||
m_repairRateLabel = makeStatLabel(m_repairSection);
|
||||
m_repairRangeLabel = makeStatLabel(m_repairSection);
|
||||
sl->addWidget(m_repairRateLabel);
|
||||
sl->addWidget(m_repairRangeLabel);
|
||||
}
|
||||
m_repairSection->setVisible(false);
|
||||
m_repairSection = new SectionBox(tr("Repair"), this);
|
||||
m_repairRateRow = new StatRow(tr("Repair rate"), m_repairSection);
|
||||
m_repairRangeRow = new StatRow(tr("Range"), m_repairSection);
|
||||
m_repairSection->getContentLayout()->addWidget(m_repairRateRow);
|
||||
m_repairSection->getContentLayout()->addWidget(m_repairRangeRow);
|
||||
m_repairSection->hide();
|
||||
layout->addWidget(m_repairSection);
|
||||
|
||||
// Current behavior — live entities only; hidden in the static design
|
||||
// preview (REQ-UI-SHIP-BEHAVIOR).
|
||||
m_behaviorLabel = makeSectionHeader(QString(), this);
|
||||
m_behaviorLabel->setVisible(false);
|
||||
layout->addWidget(m_behaviorLabel);
|
||||
// Live entities only; the design preview has no behavior to show
|
||||
// (REQ-UI-SHIP-BEHAVIOR).
|
||||
m_behaviorRow = new StatRow(tr("Behavior"), this);
|
||||
m_behaviorRow->hide();
|
||||
layout->addWidget(m_behaviorRow);
|
||||
|
||||
// Threat cost — debug-only, initially hidden.
|
||||
m_threatCostLabel = makeStatLabel(this);
|
||||
m_threatCostLabel->setVisible(false);
|
||||
layout->addWidget(m_threatCostLabel);
|
||||
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void ShipStatsPanel::refresh(const std::string& shipId,
|
||||
const std::vector<PlacedModule>& modules)
|
||||
{
|
||||
const ShipStats stats = calculateShipStats(*m_config, shipId, modules);
|
||||
const QString hpText = tr("HP: %1").arg(static_cast<int>(stats.hp + 0.5f));
|
||||
applyStats(stats, hpText);
|
||||
|
||||
const double threat = calculateShipThreatCost(m_config->threatCosts, *m_config,
|
||||
shipId, modules);
|
||||
setThreatCost(threat);
|
||||
|
||||
// The static design preview has no live behavior to show.
|
||||
m_behaviorLabel->setVisible(false);
|
||||
}
|
||||
|
||||
void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp)
|
||||
{
|
||||
const QString hpText = tr("HP: %1 / %2")
|
||||
.arg(static_cast<int>(currentHp + 0.5f))
|
||||
.arg(static_cast<int>(stats.hp + 0.5f));
|
||||
applyStats(stats, hpText);
|
||||
}
|
||||
|
||||
void ShipStatsPanel::applyStats(const ShipStats& stats, const QString& hpText)
|
||||
{
|
||||
m_hpLabel->setText(hpText);
|
||||
m_speedLabel->setText(
|
||||
tr("Max Speed: %1 tiles/s").arg(fmt(stats.maxSpeed_tps)));
|
||||
m_sensorRangeLabel->setText(
|
||||
tr("Sensor Range: %1 tiles").arg(fmt(stats.sensorRange_tiles)));
|
||||
m_mainAccelLabel->setText(
|
||||
tr("Main Accel: %1 tiles/s\xc2\xb2").arg(fmt(stats.mainAcceleration_tpss)));
|
||||
m_maneuveringAccelLabel->setText(
|
||||
tr("Maneuvering Accel: %1 tiles/s\xc2\xb2").arg(fmt(stats.maneuveringAcceleration_tpss)));
|
||||
m_angularAccelLabel->setText(
|
||||
tr("Angular Accel: %1 rad/s\xc2\xb2").arg(fmt(stats.angularAcceleration_radpss)));
|
||||
m_maxRotSpeedLabel->setText(
|
||||
tr("Max Rotation: %1 rad/s").arg(fmt(stats.maxRotationSpeed_radps)));
|
||||
|
||||
// Cargo capacity is shown only when the ship can actually hold cargo
|
||||
// (REQ-MOD-UI-STATS-PANEL).
|
||||
if (stats.cargoCapacity > 0)
|
||||
{
|
||||
m_cargoCapacityLabel->setText(
|
||||
tr("Cargo Capacity: %1").arg(stats.cargoCapacity));
|
||||
m_cargoCapacityLabel->setVisible(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_cargoCapacityLabel->setVisible(false);
|
||||
}
|
||||
|
||||
if (stats.weapons.has_value())
|
||||
{
|
||||
m_weaponDpsLabel->setText(
|
||||
tr("DPS: %1").arg(fmt(stats.weapons->combinedDps)));
|
||||
m_weaponRangeLabel->setText(
|
||||
tr("Range: %1 tiles").arg(fmt(stats.weapons->maxRange_tiles)));
|
||||
m_weaponSection->setVisible(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_weaponSection->setVisible(false);
|
||||
}
|
||||
|
||||
if (stats.salvage.has_value())
|
||||
{
|
||||
m_salvageRateLabel->setText(
|
||||
tr("Collection Rate: %1/s").arg(fmt(stats.salvage->combinedCollectionRate)));
|
||||
m_salvageRangeLabel->setText(
|
||||
tr("Range: %1 tiles").arg(fmt(stats.salvage->maxRange_tiles)));
|
||||
m_salvageSection->setVisible(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_salvageSection->setVisible(false);
|
||||
}
|
||||
|
||||
if (stats.repair.has_value())
|
||||
{
|
||||
m_repairRateLabel->setText(
|
||||
tr("Repair Rate: %1 HP/s").arg(fmt(stats.repair->combinedRepairRate_hps)));
|
||||
m_repairRangeLabel->setText(
|
||||
tr("Range: %1 tiles").arg(fmt(stats.repair->maxRange_tiles)));
|
||||
m_repairSection->setVisible(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_repairSection->setVisible(false);
|
||||
}
|
||||
// Threat cost -- shown only while debug draw is active (REQ-UI-SHIP-STATS-PANEL).
|
||||
m_threatCostRow = new StatRow(tr("Threat cost"), this);
|
||||
m_threatCostRow->hide();
|
||||
layout->addWidget(m_threatCostRow);
|
||||
}
|
||||
|
||||
void ShipStatsPanel::setBehavior(BehaviorKind kind)
|
||||
{
|
||||
QString label;
|
||||
switch (kind)
|
||||
const QString label = getBehaviorLabel(kind);
|
||||
m_behaviorRow->setValue(label);
|
||||
m_behaviorRow->setVisible(!label.isEmpty());
|
||||
}
|
||||
|
||||
void ShipStatsPanel::refresh(const std::string& shipId,
|
||||
const std::vector<PlacedModule>& modules)
|
||||
{
|
||||
const ShipStats stats = calculateShipStats(*m_config, shipId, modules);
|
||||
applyStats(stats, 1.0, QString::number(static_cast<int>(stats.hp + 0.5f)));
|
||||
|
||||
setThreatCost(calculateShipThreatCost(m_config->threatCosts, *m_config,
|
||||
shipId, modules));
|
||||
}
|
||||
|
||||
void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp)
|
||||
{
|
||||
const double fraction = (stats.hp > 0.0f)
|
||||
? static_cast<double>(currentHp) / stats.hp
|
||||
: 0.0;
|
||||
applyStats(stats, fraction, tr("%1 / %2")
|
||||
.arg(static_cast<int>(currentHp + 0.5f))
|
||||
.arg(static_cast<int>(stats.hp + 0.5f)));
|
||||
}
|
||||
|
||||
void ShipStatsPanel::applyStats(const ShipStats& stats, double hpFraction,
|
||||
const QString& hpText)
|
||||
{
|
||||
m_hpBar->setValue(hpFraction, hpText);
|
||||
m_speedRow->setValue(tr("%1 tiles/s").arg(fmt(stats.maxSpeed_tps)));
|
||||
m_sensorRangeRow->setValue(tr("%1 tiles").arg(fmt(stats.sensorRange_tiles)));
|
||||
m_mainAccelRow->setValue(
|
||||
tr("%1 tiles/s\xc2\xb2").arg(fmt(stats.mainAcceleration_tpss)));
|
||||
m_maneuveringAccelRow->setValue(
|
||||
tr("%1 tiles/s\xc2\xb2").arg(fmt(stats.maneuveringAcceleration_tpss)));
|
||||
m_angularAccelRow->setValue(
|
||||
tr("%1 rad/s\xc2\xb2").arg(fmt(stats.angularAcceleration_radpss)));
|
||||
m_maxRotSpeedRow->setValue(tr("%1 rad/s").arg(fmt(stats.maxRotationSpeed_radps)));
|
||||
|
||||
// Cargo capacity is shown only when the ship can actually hold cargo
|
||||
// (REQ-MOD-UI-STATS-PANEL).
|
||||
m_cargoCapacityRow->setVisible(stats.cargoCapacity > 0);
|
||||
if (stats.cargoCapacity > 0)
|
||||
{
|
||||
case BehaviorKind::Retreat: label = tr("Retreating"); break;
|
||||
case BehaviorKind::Attack: label = tr("Engaging"); break;
|
||||
case BehaviorKind::SalvageScrap:
|
||||
case BehaviorKind::DeliverScrap: label = tr("Salvaging"); break;
|
||||
case BehaviorKind::Repair: label = tr("Repairing"); break;
|
||||
case BehaviorKind::Rally: label = tr("Rallying"); break;
|
||||
case BehaviorKind::Standby: label = tr("Standby"); break;
|
||||
case BehaviorKind::Advance: label = tr("Advancing"); break;
|
||||
case BehaviorKind::None: break;
|
||||
m_cargoCapacityRow->setValue(QString::number(stats.cargoCapacity));
|
||||
}
|
||||
|
||||
if (label.isEmpty())
|
||||
m_weaponSection->setVisible(stats.weapons.has_value());
|
||||
if (stats.weapons.has_value())
|
||||
{
|
||||
m_behaviorLabel->setVisible(false);
|
||||
return;
|
||||
m_weaponDpsRow->setValue(fmt(stats.weapons->combinedDps));
|
||||
m_weaponRangeRow->setValue(tr("%1 tiles").arg(fmt(stats.weapons->maxRange_tiles)));
|
||||
}
|
||||
|
||||
m_behaviorLabel->setText(tr("Behavior: %1").arg(label));
|
||||
m_behaviorLabel->setVisible(true);
|
||||
m_salvageSection->setVisible(stats.salvage.has_value());
|
||||
if (stats.salvage.has_value())
|
||||
{
|
||||
m_salvageRateRow->setValue(
|
||||
tr("%1 /s").arg(fmt(stats.salvage->combinedCollectionRate)));
|
||||
m_salvageRangeRow->setValue(tr("%1 tiles").arg(fmt(stats.salvage->maxRange_tiles)));
|
||||
}
|
||||
|
||||
m_repairSection->setVisible(stats.repair.has_value());
|
||||
if (stats.repair.has_value())
|
||||
{
|
||||
m_repairRateRow->setValue(
|
||||
tr("%1 HP/s").arg(fmt(stats.repair->combinedRepairRate_hps)));
|
||||
m_repairRangeRow->setValue(tr("%1 tiles").arg(fmt(stats.repair->maxRange_tiles)));
|
||||
}
|
||||
}
|
||||
|
||||
void ShipStatsPanel::setThreatCost(double cost)
|
||||
{
|
||||
m_threatCostLabel->setText(tr("Threat Cost: %1").arg(cost, 0, 'f', 1));
|
||||
m_threatCostLabel->setVisible(m_debugDraw);
|
||||
m_threatCostRow->setValue(QString::number(cost, 'f', 1));
|
||||
m_threatCostRow->setVisible(m_debugDraw);
|
||||
}
|
||||
|
||||
void ShipStatsPanel::setDebugDrawEnabled(bool enabled)
|
||||
{
|
||||
m_debugDraw = enabled;
|
||||
m_threatCostLabel->setVisible(m_debugDraw);
|
||||
m_threatCostRow->setVisible(m_debugDraw);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "BehaviorKind.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipStatsCalculator.h"
|
||||
|
||||
struct GameConfig;
|
||||
class QLabel;
|
||||
#include "BehaviorKind.h"
|
||||
|
||||
struct GameConfig;
|
||||
class BarRow;
|
||||
class SectionBox;
|
||||
class StatRow;
|
||||
|
||||
// The hull stats and capability module summaries of one ship, as a bar for HP and a
|
||||
// label/value row for everything else. Shared by the live selection card
|
||||
// (REQ-UI-SHIP-STATS-PANEL), the layout configuration dialog's design preview
|
||||
// (REQ-MOD-UI-STATS-PANEL) and the balancing tool, so all three read alike.
|
||||
//
|
||||
// The behavior row is for consumers with no header to put it in. The selection card
|
||||
// shows the behavior in its header instead (REQ-UI-SHIP-BEHAVIOR) and leaves the row
|
||||
// unset; the design preview has no live ship to have a behavior at all.
|
||||
class ShipStatsPanel : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -20,44 +30,47 @@ class ShipStatsPanel : public QWidget
|
||||
public:
|
||||
explicit ShipStatsPanel(const GameConfig* config, QWidget* parent = nullptr);
|
||||
|
||||
// Stats of a design rather than of a live ship: the HP bar reads full, because the
|
||||
// number shown is the maximum the design would have.
|
||||
void refresh(const std::string& shipId,
|
||||
const std::vector<PlacedModule>& modules);
|
||||
|
||||
void refreshFromLive(const ShipStats& stats, float currentHp);
|
||||
|
||||
// Displays the ship's current top-priority behavior (REQ-UI-SHIP-BEHAVIOR).
|
||||
// Shows the ship's top-priority behavior as a row of its own. Never called by the
|
||||
// selection card, which has a header slot for it.
|
||||
void setBehavior(BehaviorKind kind);
|
||||
|
||||
void setThreatCost(double cost);
|
||||
void setDebugDrawEnabled(bool enabled);
|
||||
|
||||
private:
|
||||
void applyStats(const ShipStats& stats, const QString& hpText);
|
||||
void applyStats(const ShipStats& stats, double hpFraction, const QString& hpText);
|
||||
|
||||
const GameConfig* m_config;
|
||||
bool m_debugDraw = false;
|
||||
|
||||
QLabel* m_behaviorLabel;
|
||||
QLabel* m_hpLabel;
|
||||
QLabel* m_speedLabel;
|
||||
QLabel* m_sensorRangeLabel;
|
||||
QLabel* m_mainAccelLabel;
|
||||
QLabel* m_maneuveringAccelLabel;
|
||||
QLabel* m_angularAccelLabel;
|
||||
QLabel* m_maxRotSpeedLabel;
|
||||
QLabel* m_cargoCapacityLabel;
|
||||
BarRow* m_hpBar;
|
||||
StatRow* m_speedRow;
|
||||
StatRow* m_sensorRangeRow;
|
||||
StatRow* m_mainAccelRow;
|
||||
StatRow* m_maneuveringAccelRow;
|
||||
StatRow* m_angularAccelRow;
|
||||
StatRow* m_maxRotSpeedRow;
|
||||
StatRow* m_cargoCapacityRow;
|
||||
|
||||
QWidget* m_weaponSection;
|
||||
QLabel* m_weaponDpsLabel;
|
||||
QLabel* m_weaponRangeLabel;
|
||||
SectionBox* m_weaponSection;
|
||||
StatRow* m_weaponDpsRow;
|
||||
StatRow* m_weaponRangeRow;
|
||||
|
||||
QWidget* m_salvageSection;
|
||||
QLabel* m_salvageRateLabel;
|
||||
QLabel* m_salvageRangeLabel;
|
||||
SectionBox* m_salvageSection;
|
||||
StatRow* m_salvageRateRow;
|
||||
StatRow* m_salvageRangeRow;
|
||||
|
||||
QWidget* m_repairSection;
|
||||
QLabel* m_repairRateLabel;
|
||||
QLabel* m_repairRangeLabel;
|
||||
SectionBox* m_repairSection;
|
||||
StatRow* m_repairRateRow;
|
||||
StatRow* m_repairRangeRow;
|
||||
|
||||
QLabel* m_threatCostLabel;
|
||||
StatRow* m_behaviorRow;
|
||||
StatRow* m_threatCostRow;
|
||||
};
|
||||
|
||||
44
src/ui/selection/AutoProductionContent.cpp
Normal file
44
src/ui/selection/AutoProductionContent.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "AutoProductionContent.h"
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingTarget.h"
|
||||
#include "GameConfig.h"
|
||||
|
||||
AutoProductionContent::AutoProductionContent(const SelectionContext& context,
|
||||
const SelectionRequest& request,
|
||||
QWidget* parent)
|
||||
: BufferedBuildingContent(context, request.buildings.front(), parent)
|
||||
{
|
||||
}
|
||||
|
||||
BufferedBuildingContent::CycleInfo AutoProductionContent::getCycleInfo(
|
||||
const BuildingTarget& target) 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 to describe.
|
||||
info.runsProduction = true;
|
||||
if (!target.building || !target.building->production.has_value())
|
||||
{
|
||||
return info;
|
||||
}
|
||||
|
||||
const RecipeDef* recipe = getContext().config->recipes.findRecipeDef(
|
||||
target.building->production->recipeId, target.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;
|
||||
}
|
||||
20
src/ui/selection/AutoProductionContent.h
Normal file
20
src/ui/selection/AutoProductionContent.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#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:
|
||||
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
||||
};
|
||||
118
src/ui/selection/BarRow.cpp
Normal file
118
src/ui/selection/BarRow.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include "BarRow.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Height and corner radius of the fill bar, in device-independent pixels.
|
||||
const int kBarHeightPx = 6;
|
||||
const qreal kBarRadiusPx = 3.0;
|
||||
|
||||
// Opacity of the unfilled track, over the card's background.
|
||||
const int kTrackAlpha = 60;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
// The bar itself. Painted rather than assembled from a QProgressBar, because all it
|
||||
// needs is a rounded track with a rounded fill and a style sheet cannot be relied on to
|
||||
// leave a progress bar's groove and chunk alone across styles.
|
||||
class BarRow::Bar : public QWidget
|
||||
{
|
||||
public:
|
||||
explicit Bar(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_fraction(0.0)
|
||||
{
|
||||
setFixedHeight(kBarHeightPx);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
}
|
||||
|
||||
void setFraction(double fraction)
|
||||
{
|
||||
m_fraction = std::max(0.0, std::min(1.0, fraction));
|
||||
update();
|
||||
}
|
||||
|
||||
void setFillColor(const QColor& color)
|
||||
{
|
||||
m_fillColor = color;
|
||||
update();
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* /*event*/) override
|
||||
{
|
||||
const QColor fill = m_fillColor.isValid()
|
||||
? m_fillColor
|
||||
: palette().color(QPalette::Highlight);
|
||||
|
||||
QColor track = fill;
|
||||
track.setAlpha(kTrackAlpha);
|
||||
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setPen(Qt::NoPen);
|
||||
|
||||
painter.setBrush(track);
|
||||
painter.drawRoundedRect(rect(), kBarRadiusPx, kBarRadiusPx);
|
||||
|
||||
if (m_fraction > 0.0)
|
||||
{
|
||||
QRect filled = rect();
|
||||
filled.setWidth(static_cast<int>(filled.width() * m_fraction));
|
||||
painter.setBrush(fill);
|
||||
painter.drawRoundedRect(filled, kBarRadiusPx, kBarRadiusPx);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
double m_fraction;
|
||||
QColor m_fillColor;
|
||||
};
|
||||
|
||||
|
||||
BarRow::BarRow(const QString& caption, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(2);
|
||||
|
||||
QWidget* captionRow = new QWidget(this);
|
||||
QHBoxLayout* captionLayout = new QHBoxLayout(captionRow);
|
||||
captionLayout->setContentsMargins(0, 0, 0, 0);
|
||||
captionLayout->setSpacing(8);
|
||||
|
||||
m_captionLabel = new QLabel(caption, captionRow);
|
||||
m_captionLabel->setVisible(!caption.isEmpty());
|
||||
m_valueLabel = new QLabel(captionRow);
|
||||
m_valueLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
|
||||
captionLayout->addWidget(m_captionLabel);
|
||||
captionLayout->addStretch(1);
|
||||
captionLayout->addWidget(m_valueLabel);
|
||||
|
||||
m_bar = new Bar(this);
|
||||
|
||||
layout->addWidget(captionRow);
|
||||
layout->addWidget(m_bar);
|
||||
}
|
||||
|
||||
void BarRow::setValue(double fraction, const QString& valueText)
|
||||
{
|
||||
m_bar->setFraction(fraction);
|
||||
m_valueLabel->setText(valueText);
|
||||
}
|
||||
|
||||
void BarRow::setFillColor(const QColor& color)
|
||||
{
|
||||
m_bar->setFillColor(color);
|
||||
}
|
||||
36
src/ui/selection/BarRow.h
Normal file
36
src/ui/selection/BarRow.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
|
||||
// A caption with its value hard right and a horizontal fill bar beneath it. One part for
|
||||
// the three things the panel shows as a proportion: a construction site's progress, a
|
||||
// building's production cycle, and the HP of a ship, a station or the HQ
|
||||
// (REQ-UI-PRODUCTION-PROGRESS, REQ-UI-SHIP-STATS-PANEL, REQ-UI-STATION-STATS-PANEL,
|
||||
// REQ-UI-HQ-PANEL).
|
||||
//
|
||||
// The caption may be left empty, for a bar whose meaning is already given by the section
|
||||
// it sits in.
|
||||
class BarRow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BarRow(const QString& caption, QWidget* parent = nullptr);
|
||||
|
||||
// fraction is clamped to [0, 1]; valueText is shown beside the caption as-is, so a
|
||||
// bar can read "72%" or "340 / 500" as its meaning requires.
|
||||
void setValue(double fraction, const QString& valueText);
|
||||
// Overrides the fill color, which defaults to the palette's highlight.
|
||||
void setFillColor(const QColor& color);
|
||||
|
||||
private:
|
||||
class Bar;
|
||||
|
||||
QLabel* m_captionLabel;
|
||||
QLabel* m_valueLabel;
|
||||
Bar* m_bar;
|
||||
};
|
||||
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;
|
||||
};
|
||||
103
src/ui/selection/BufferSection.cpp
Normal file
103
src/ui/selection/BufferSection.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "BufferSection.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Building.h"
|
||||
#include "DisplayName.h"
|
||||
#include "ItemChipRow.h"
|
||||
#include "SectionBox.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
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(const SelectionContext& context, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(6);
|
||||
|
||||
m_inputSection = new SectionBox(tr("Input buffers"), this);
|
||||
m_inputChips = new ItemChipRow(context.itemIcons, m_inputSection);
|
||||
m_inputSection->getContentLayout()->addWidget(m_inputChips);
|
||||
|
||||
m_outputSection = new SectionBox(tr("Output buffer"), this);
|
||||
m_outputChips = new ItemChipRow(context.itemIcons, m_outputSection);
|
||||
m_outputSection->getContentLayout()->addWidget(m_outputChips);
|
||||
|
||||
layout->addWidget(m_inputSection);
|
||||
layout->addWidget(m_outputSection);
|
||||
}
|
||||
|
||||
void BufferSection::setBuffers(const Building& building,
|
||||
const std::map<std::string, int>& perCycleInputs,
|
||||
const std::map<std::string, int>& perCycleOutputs)
|
||||
{
|
||||
std::vector<ItemChipRow::Entry> inputs;
|
||||
for (const std::pair<const ItemType, int>& entry : building.inputBuffer.counts)
|
||||
{
|
||||
ItemChipRow::Entry chip;
|
||||
chip.itemId = entry.first.id;
|
||||
chip.countText = QString::number(entry.second);
|
||||
|
||||
const int perCycle = findPerCycle(perCycleInputs, entry.first.id);
|
||||
if (perCycle > 0)
|
||||
{
|
||||
chip.subLine = tr("/ %1 per cycle").arg(perCycle);
|
||||
}
|
||||
inputs.push_back(chip);
|
||||
}
|
||||
|
||||
// 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 everything its cycle produces, so an output the player
|
||||
// is waiting for reads as 0 rather than being absent.
|
||||
for (const std::pair<const std::string, int>& entry : perCycleOutputs)
|
||||
{
|
||||
outputCounts.emplace(entry.first, 0);
|
||||
}
|
||||
|
||||
std::vector<ItemChipRow::Entry> outputs;
|
||||
for (const std::pair<const std::string, int>& entry : outputCounts)
|
||||
{
|
||||
ItemChipRow::Entry chip;
|
||||
chip.itemId = entry.first;
|
||||
// Counted against the buffer's capacity, which is what production stops at
|
||||
// (REQ-MAT-OUTPUT-BUFFER).
|
||||
chip.countText = building.outputBuffer.capacity > 0
|
||||
? tr("%1 / %2").arg(entry.second).arg(building.outputBuffer.capacity)
|
||||
: QString::number(entry.second);
|
||||
chip.subLine = QString::fromStdString(toDisplayName(entry.first));
|
||||
outputs.push_back(chip);
|
||||
}
|
||||
|
||||
m_inputChips->setEntries(inputs);
|
||||
m_outputChips->setEntries(outputs);
|
||||
m_inputSection->setVisible(!inputs.empty());
|
||||
m_outputSection->setVisible(!outputs.empty());
|
||||
setVisible(!inputs.empty() || !outputs.empty());
|
||||
}
|
||||
39
src/ui/selection/BufferSection.h
Normal file
39
src/ui/selection/BufferSection.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "SelectionContext.h"
|
||||
|
||||
struct Building;
|
||||
class ItemChipRow;
|
||||
class SectionBox;
|
||||
|
||||
// The input and output buffer contents of one building, each a captioned section of item
|
||||
// chips (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,
|
||||
// and a section holding nothing is not shown at all.
|
||||
class BufferSection : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BufferSection(const SelectionContext& context, 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:
|
||||
SectionBox* m_inputSection;
|
||||
ItemChipRow* m_inputChips;
|
||||
SectionBox* m_outputSection;
|
||||
ItemChipRow* m_outputChips;
|
||||
};
|
||||
84
src/ui/selection/BufferedBuildingContent.cpp
Normal file
84
src/ui/selection/BufferedBuildingContent.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
#include "BufferedBuildingContent.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Building.h"
|
||||
#include "BufferSection.h"
|
||||
#include "BuildingTarget.h"
|
||||
#include "FactoryQueries.h"
|
||||
#include "ProductionSection.h"
|
||||
#include "RecipeSummaryRow.h"
|
||||
#include "SelectionNames.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::vector<RecipeSummaryRow::Amount> toAmounts(const std::map<std::string, int>& map)
|
||||
{
|
||||
std::vector<RecipeSummaryRow::Amount> amounts;
|
||||
amounts.reserve(map.size());
|
||||
for (const std::pair<const std::string, int>& entry : map)
|
||||
{
|
||||
amounts.push_back(RecipeSummaryRow::Amount{ entry.first, entry.second });
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
BufferedBuildingContent::BufferedBuildingContent(const SelectionContext& context,
|
||||
BuildingId id, QWidget* parent)
|
||||
: SelectionContent(context, asConstructionSite(context, id), parent)
|
||||
, m_id(id)
|
||||
{
|
||||
// The summary is configuration -- what the building will do -- so it sits with the
|
||||
// selection control and is shown for a construction site too (REQ-UI-RECIPE-SUMMARY).
|
||||
m_recipeSummary = new RecipeSummaryRow(context.itemIcons, this);
|
||||
getConfigurationLayout()->addWidget(m_recipeSummary);
|
||||
|
||||
m_buffers = new BufferSection(context, this);
|
||||
m_production = new ProductionSection(this);
|
||||
getRuntimeLayout()->addWidget(m_buffers);
|
||||
getRuntimeLayout()->addWidget(m_production);
|
||||
}
|
||||
|
||||
void BufferedBuildingContent::refreshConfiguration()
|
||||
{
|
||||
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
||||
if (!target.isValid())
|
||||
{
|
||||
// Gone under the card. SelectionPanel rebuilds on the same refresh; this only
|
||||
// has to avoid reading it.
|
||||
return;
|
||||
}
|
||||
|
||||
setBuildingIdentity(target.type, getBuildingTypeName(target.type));
|
||||
|
||||
const CycleInfo cycle = getCycleInfo(target);
|
||||
m_recipeSummary->setSummary(toAmounts(cycle.perCycleInputs),
|
||||
toAmounts(cycle.perCycleOutputs),
|
||||
cycle.durationSeconds);
|
||||
|
||||
refreshControls(target);
|
||||
}
|
||||
|
||||
void BufferedBuildingContent::refreshRuntime()
|
||||
{
|
||||
const BuildingTarget target = resolveBuildingTarget(getContext(), m_id);
|
||||
if (!target.building)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
setProductionStatusSlot(*target.building);
|
||||
|
||||
const CycleInfo cycle = getCycleInfo(target);
|
||||
m_buffers->setBuffers(*target.building, cycle.perCycleInputs, cycle.perCycleOutputs);
|
||||
m_production->setProduction(cycle.runsProduction, *target.building,
|
||||
cycle.durationSeconds,
|
||||
getContext().sim->getCurrentTick());
|
||||
}
|
||||
62
src/ui/selection/BufferedBuildingContent.h
Normal file
62
src/ui/selection/BufferedBuildingContent.h
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "BuildingId.h"
|
||||
#include "SelectionContent.h"
|
||||
|
||||
struct BuildingTarget;
|
||||
class BufferSection;
|
||||
class ProductionSection;
|
||||
class RecipeSummaryRow;
|
||||
|
||||
// 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 identity and status, recipe
|
||||
// summary, 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);
|
||||
|
||||
// Called with a construction site's stored configuration too, so the summary of what
|
||||
// the building will produce is shown before it is built (REQ-BLD-SITE-CONFIG).
|
||||
virtual CycleInfo getCycleInfo(const BuildingTarget& target) const = 0;
|
||||
|
||||
// The subclass's own configuration controls. The identity, the recipe summary, the
|
||||
// buffers and the production progress are handled here.
|
||||
virtual void refreshControls(const BuildingTarget& /*target*/) {}
|
||||
|
||||
BuildingId getBuildingId() const { return m_id; }
|
||||
|
||||
private:
|
||||
void refreshConfiguration() override;
|
||||
void refreshRuntime() override;
|
||||
|
||||
BuildingId m_id;
|
||||
RecipeSummaryRow* m_recipeSummary;
|
||||
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);
|
||||
78
src/ui/selection/CMakeLists.txt
Normal file
78
src/ui/selection/CMakeLists.txt
Normal file
@@ -0,0 +1,78 @@
|
||||
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}/StatRow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BarRow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SectionBox.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CountRow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/StatusPill.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.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}/StatRow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BarRow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SectionBox.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CountRow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/StatusPill.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EmptyNote.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChip.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemChipRow.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSummaryRow.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;
|
||||
};
|
||||
29
src/ui/selection/CountRow.cpp
Normal file
29
src/ui/selection/CountRow.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#include "CountRow.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
|
||||
CountRow::CountRow(const QPixmap& symbol, const QString& name, int count,
|
||||
QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(6);
|
||||
|
||||
m_symbolLabel = new QLabel(this);
|
||||
m_symbolLabel->setPixmap(symbol);
|
||||
m_symbolLabel->setVisible(!symbol.isNull());
|
||||
|
||||
m_nameLabel = new QLabel(name, this);
|
||||
|
||||
// The same "x<count>" notation the recipe tooltip and the header's aggregate count
|
||||
// use (REQ-UI-MULTI-SELECTION).
|
||||
m_countLabel = new QLabel(tr("x%1").arg(count), this);
|
||||
m_countLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
|
||||
layout->addWidget(m_symbolLabel);
|
||||
layout->addWidget(m_nameLabel);
|
||||
layout->addStretch(1);
|
||||
layout->addWidget(m_countLabel);
|
||||
}
|
||||
25
src/ui/selection/CountRow.h
Normal file
25
src/ui/selection/CountRow.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
|
||||
// One "<symbol> <name> x<count>" line of a count summary. The same part serves the
|
||||
// building summary and the field summary, which count different things the same way
|
||||
// (REQ-UI-MULTI-SELECTION, REQ-UI-FIELD-MULTI-SELECTION).
|
||||
class CountRow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// An empty symbol leaves the icon off, for the kinds of object that have none.
|
||||
CountRow(const QPixmap& symbol, const QString& name, int count,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
QLabel* m_symbolLabel;
|
||||
QLabel* m_nameLabel;
|
||||
QLabel* m_countLabel;
|
||||
};
|
||||
32
src/ui/selection/DebrisContent.cpp
Normal file
32
src/ui/selection/DebrisContent.cpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#include "DebrisContent.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "DebrisScrap.h"
|
||||
#include "Simulation.h"
|
||||
#include "StatRow.h"
|
||||
|
||||
DebrisContent::DebrisContent(const SelectionContext& context,
|
||||
const SelectionRequest& request, QWidget* parent)
|
||||
: SelectionContent(context, std::nullopt, parent)
|
||||
, m_debris(request.debris)
|
||||
{
|
||||
m_scrapRow = new StatRow(tr("Scrap remaining"), this);
|
||||
m_scrapRow->setValueEmphasized(true);
|
||||
getRuntimeLayout()->addWidget(m_scrapRow);
|
||||
|
||||
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. With
|
||||
// several pieces selected it is their sum (REQ-UI-SELECTION-AGGREGATE).
|
||||
m_scrapRow->setValue(QString::number(
|
||||
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 StatRow;
|
||||
|
||||
// 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;
|
||||
StatRow* m_scrapRow;
|
||||
};
|
||||
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);
|
||||
19
src/ui/selection/EmptyNote.cpp
Normal file
19
src/ui/selection/EmptyNote.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "EmptyNote.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QPalette>
|
||||
|
||||
EmptyNote::EmptyNote(const QString& text, QWidget* parent)
|
||||
: QLabel(text, parent)
|
||||
{
|
||||
setWordWrap(true);
|
||||
|
||||
QFont noteFont = font();
|
||||
noteFont.setItalic(true);
|
||||
setFont(noteFont);
|
||||
|
||||
QPalette notePalette = palette();
|
||||
notePalette.setColor(QPalette::WindowText,
|
||||
palette().color(QPalette::Disabled, QPalette::WindowText));
|
||||
setPalette(notePalette);
|
||||
}
|
||||
15
src/ui/selection/EmptyNote.h
Normal file
15
src/ui/selection/EmptyNote.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QLabel>
|
||||
|
||||
// A dimmed aside explaining why a part of the card is not there yet -- a construction
|
||||
// site's "no buffers until built" (REQ-UI-SELECTION-CARD). Styled apart from the card's
|
||||
// values so it reads as an explanation rather than as data.
|
||||
class EmptyNote : public QLabel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EmptyNote(const QString& text, QWidget* parent = nullptr);
|
||||
};
|
||||
105
src/ui/selection/FieldMultiContent.cpp
Normal file
105
src/ui/selection/FieldMultiContent.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "FieldMultiContent.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "CountRow.h"
|
||||
#include "DebrisScrap.h"
|
||||
#include "DisplayName.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "Simulation.h"
|
||||
#include "StatRow.h"
|
||||
#include "StationBodyComponent.h"
|
||||
|
||||
FieldMultiContent::FieldMultiContent(const SelectionContext& context,
|
||||
const SelectionRequest& request, QWidget* parent)
|
||||
: SelectionContent(context, std::nullopt, parent)
|
||||
, m_debris(request.debris)
|
||||
, m_scrapRow(nullptr)
|
||||
{
|
||||
setIdentity(QPixmap(), tr("Mixed selection"));
|
||||
setCountSlot(static_cast<int>(request.actors.size() + request.debris.size()));
|
||||
|
||||
buildSummary(request.actors);
|
||||
}
|
||||
|
||||
void FieldMultiContent::buildSummary(const std::vector<entt::entity>& actors)
|
||||
{
|
||||
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 : 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;
|
||||
}
|
||||
|
||||
for (const QString& key : keys)
|
||||
{
|
||||
getRuntimeLayout()->addWidget(
|
||||
new CountRow(QPixmap(), labels[key], counts[key], this));
|
||||
}
|
||||
|
||||
if (!m_debris.empty())
|
||||
{
|
||||
getRuntimeLayout()->addWidget(new CountRow(
|
||||
QPixmap(), tr("Debris"), static_cast<int>(m_debris.size()), this));
|
||||
|
||||
// Indented under the debris row, so the total reads as belonging to it
|
||||
// (REQ-UI-DEBRIS-PANEL).
|
||||
m_scrapRow = new StatRow(tr("holding"), this);
|
||||
m_scrapRow->setIndented(true);
|
||||
m_scrapRow->setValueEmphasized(true);
|
||||
getRuntimeLayout()->addWidget(m_scrapRow);
|
||||
}
|
||||
}
|
||||
|
||||
void FieldMultiContent::refreshRuntime()
|
||||
{
|
||||
// The counts are fixed for a given selection -- an actor leaving it re-publishes the
|
||||
// selection and rebuilds this card -- but the scrap falls as the debris is collected.
|
||||
if (m_scrapRow)
|
||||
{
|
||||
m_scrapRow->setValue(tr("%1 scrap")
|
||||
.arg(sumDebrisScrap(getContext().sim->getAdmin(), m_debris)));
|
||||
}
|
||||
}
|
||||
33
src/ui/selection/FieldMultiContent.h
Normal file
33
src/ui/selection/FieldMultiContent.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
#include "SelectionContent.h"
|
||||
#include "SelectionContentFactory.h"
|
||||
|
||||
class StatRow;
|
||||
|
||||
// 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:
|
||||
void buildSummary(const std::vector<entt::entity>& actors);
|
||||
|
||||
std::vector<entt::entity> m_debris;
|
||||
// Null unless debris is part of the selection; the only value here that changes
|
||||
// while the selection stands.
|
||||
StatRow* m_scrapRow;
|
||||
};
|
||||
60
src/ui/selection/HqContent.cpp
Normal file
60
src/ui/selection/HqContent.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "HqContent.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BarRow.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "HqProxyComponent.h"
|
||||
#include "IconCaption.h"
|
||||
#include "ItemChipRow.h"
|
||||
#include "SectionBox.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_stockSection = new SectionBox(tr("Building blocks"), this);
|
||||
m_stockChips = new ItemChipRow(context.itemIcons, m_stockSection);
|
||||
m_stockSection->getContentLayout()->addWidget(m_stockChips);
|
||||
|
||||
m_hpBar = new BarRow(tr("HP"), this);
|
||||
|
||||
getRuntimeLayout()->addWidget(m_stockSection);
|
||||
getRuntimeLayout()->addWidget(m_hpBar);
|
||||
|
||||
setBuildingIdentity(BuildingType::Hq, getBuildingTypeName(BuildingType::Hq));
|
||||
}
|
||||
|
||||
void HqContent::refreshRuntime()
|
||||
{
|
||||
// Not a buffer: blocks delivered by belt go straight into the global stock
|
||||
// (REQ-HQ-BELT-INPUT). Showing it here is what tells the player to route them here
|
||||
// (REQ-UI-HQ-PANEL).
|
||||
ItemChipRow::Entry stock;
|
||||
stock.itemId = kBlockItemId;
|
||||
stock.countText = QString::number(getContext().sim->getBuildingBlocksStock());
|
||||
stock.subLine = tr("in stock");
|
||||
m_stockChips->setEntries(std::vector<ItemChipRow::Entry>{ stock });
|
||||
|
||||
// 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)
|
||||
{
|
||||
const double fraction = (health.maxHp > 0.0f)
|
||||
? static_cast<double>(health.hp) / health.maxHp
|
||||
: 0.0;
|
||||
m_hpBar->setValue(fraction, tr("%1 / %2")
|
||||
.arg(static_cast<int>(health.hp + 0.5f))
|
||||
.arg(static_cast<int>(health.maxHp + 0.5f)));
|
||||
});
|
||||
}
|
||||
32
src/ui/selection/HqContent.h
Normal file
32
src/ui/selection/HqContent.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "SelectionContent.h"
|
||||
#include "SelectionContentFactory.h"
|
||||
|
||||
class BarRow;
|
||||
class ItemChipRow;
|
||||
class SectionBox;
|
||||
|
||||
// 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:
|
||||
SectionBox* m_stockSection;
|
||||
ItemChipRow* m_stockChips;
|
||||
BarRow* m_hpBar;
|
||||
};
|
||||
72
src/ui/selection/ItemChip.cpp
Normal file
72
src/ui/selection/ItemChip.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "ItemChip.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPalette>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Point-size rise of the count and drop of the sub-line, relative to the card's text.
|
||||
const int kCountSizeRisePt = 2;
|
||||
const int kSubLineSizeDropPt = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
ItemChip::ItemChip(const QPixmap& icon, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
// Its own boxed chrome, drawn with palette colors like the rest of the panel's
|
||||
// furniture rather than from visuals.toml, which is for world rendering.
|
||||
setAttribute(Qt::WA_StyledBackground, true);
|
||||
setStyleSheet(QStringLiteral(
|
||||
"ItemChip { border: 1px solid palette(mid); border-radius: 3px; }"));
|
||||
|
||||
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(4, 3, 4, 3);
|
||||
layout->setSpacing(6);
|
||||
|
||||
m_iconLabel = new QLabel(this);
|
||||
m_iconLabel->setPixmap(icon);
|
||||
m_iconLabel->setVisible(!icon.isNull());
|
||||
|
||||
QWidget* text = new QWidget(this);
|
||||
QVBoxLayout* textLayout = new QVBoxLayout(text);
|
||||
textLayout->setContentsMargins(0, 0, 0, 0);
|
||||
textLayout->setSpacing(0);
|
||||
|
||||
m_countLabel = new QLabel(text);
|
||||
QFont countFont = m_countLabel->font();
|
||||
countFont.setPointSize(countFont.pointSize() + kCountSizeRisePt);
|
||||
m_countLabel->setFont(countFont);
|
||||
|
||||
m_subLineLabel = new QLabel(text);
|
||||
QFont subLineFont = m_subLineLabel->font();
|
||||
subLineFont.setPointSize(qMax(1, subLineFont.pointSize() - kSubLineSizeDropPt));
|
||||
m_subLineLabel->setFont(subLineFont);
|
||||
QPalette subLinePalette = m_subLineLabel->palette();
|
||||
subLinePalette.setColor(QPalette::WindowText,
|
||||
palette().color(QPalette::Disabled, QPalette::WindowText));
|
||||
m_subLineLabel->setPalette(subLinePalette);
|
||||
m_subLineLabel->hide();
|
||||
|
||||
textLayout->addWidget(m_countLabel);
|
||||
textLayout->addWidget(m_subLineLabel);
|
||||
|
||||
layout->addWidget(m_iconLabel);
|
||||
layout->addWidget(text);
|
||||
}
|
||||
|
||||
void ItemChip::setCount(const QString& count)
|
||||
{
|
||||
m_countLabel->setText(count);
|
||||
}
|
||||
|
||||
void ItemChip::setSubLine(const QString& subLine)
|
||||
{
|
||||
m_subLineLabel->setText(subLine);
|
||||
m_subLineLabel->setVisible(!subLine.isEmpty());
|
||||
}
|
||||
28
src/ui/selection/ItemChip.h
Normal file
28
src/ui/selection/ItemChip.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
|
||||
// One buffered item: its icon, its count in a larger type, and a sub-line beneath the
|
||||
// count (REQ-UI-SINGLE-SELECTION). Boxed so a row of them reads as separate quantities
|
||||
// rather than as a run of text.
|
||||
class ItemChip : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// An empty icon leaves the icon off and the chip laid out around the text alone.
|
||||
ItemChip(const QPixmap& icon, QWidget* parent = nullptr);
|
||||
|
||||
void setCount(const QString& count);
|
||||
// Left empty for an item with nothing to say beneath its count.
|
||||
void setSubLine(const QString& subLine);
|
||||
|
||||
private:
|
||||
QLabel* m_iconLabel;
|
||||
QLabel* m_countLabel;
|
||||
QLabel* m_subLineLabel;
|
||||
};
|
||||
78
src/ui/selection/ItemChipRow.cpp
Normal file
78
src/ui/selection/ItemChipRow.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#include "ItemChipRow.h"
|
||||
|
||||
#include <QGridLayout>
|
||||
|
||||
#include "ItemChip.h"
|
||||
#include "ItemIconCache.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Size the item icon is drawn at inside a chip, in device-independent pixels.
|
||||
const int kChipIconSizePx = 18;
|
||||
|
||||
// Chips per row. Two fit the panel's capped width side by side; a third would force the
|
||||
// counts to shrink.
|
||||
const int kChipsPerRow = 2;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
ItemChipRow::ItemChipRow(ItemIconCache* itemIcons, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_itemIcons(itemIcons)
|
||||
{
|
||||
m_layout = new QGridLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout->setSpacing(4);
|
||||
}
|
||||
|
||||
void ItemChipRow::setEntries(const std::vector<Entry>& entries)
|
||||
{
|
||||
std::vector<std::string> itemIds;
|
||||
itemIds.reserve(entries.size());
|
||||
for (const Entry& entry : entries)
|
||||
{
|
||||
itemIds.push_back(entry.itemId);
|
||||
}
|
||||
|
||||
// Only the set of items changing is a structural change; the counts change every
|
||||
// tick and must not cost a widget rebuild.
|
||||
if (itemIds != m_itemIds)
|
||||
{
|
||||
m_itemIds = std::move(itemIds);
|
||||
rebuildChips(entries);
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < entries.size(); ++index)
|
||||
{
|
||||
m_chips[index]->setCount(entries[index].countText);
|
||||
m_chips[index]->setSubLine(entries[index].subLine);
|
||||
}
|
||||
setVisible(!entries.empty());
|
||||
}
|
||||
|
||||
void ItemChipRow::rebuildChips(const std::vector<Entry>& entries)
|
||||
{
|
||||
for (ItemChip* chip : m_chips)
|
||||
{
|
||||
m_layout->removeWidget(chip);
|
||||
chip->deleteLater();
|
||||
}
|
||||
m_chips.clear();
|
||||
|
||||
for (std::size_t index = 0; index < entries.size(); ++index)
|
||||
{
|
||||
const std::string& itemId = entries[index].itemId;
|
||||
// A missing icon file is not an error (REQ-UI-ITEM-ICON): the chip is then laid
|
||||
// out around its count alone.
|
||||
const QPixmap icon = m_itemIcons->hasIcon(itemId)
|
||||
? m_itemIcons->getPixmap(itemId, kChipIconSizePx)
|
||||
: QPixmap();
|
||||
|
||||
ItemChip* chip = new ItemChip(icon, this);
|
||||
m_layout->addWidget(chip, static_cast<int>(index) / kChipsPerRow,
|
||||
static_cast<int>(index) % kChipsPerRow);
|
||||
m_chips.push_back(chip);
|
||||
}
|
||||
}
|
||||
44
src/ui/selection/ItemChipRow.h
Normal file
44
src/ui/selection/ItemChipRow.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class ItemChip;
|
||||
class ItemIconCache;
|
||||
class QGridLayout;
|
||||
|
||||
// The buffered items of one building, each as a chip carrying the item's icon, its
|
||||
// current count and a sub-line (REQ-UI-SINGLE-SELECTION): the per-cycle amount for an
|
||||
// input, the item's name for an output.
|
||||
//
|
||||
// The chips are rebuilt only when the set of items changes, not when their counts do, so
|
||||
// a refresh at tick rate updates numbers instead of churning widgets.
|
||||
class ItemChipRow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct Entry
|
||||
{
|
||||
std::string itemId;
|
||||
QString countText;
|
||||
QString subLine;
|
||||
};
|
||||
|
||||
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); an item with
|
||||
// no icon file simply shows no icon, which is not an error. Not owned.
|
||||
explicit ItemChipRow(ItemIconCache* itemIcons, QWidget* parent = nullptr);
|
||||
|
||||
void setEntries(const std::vector<Entry>& entries);
|
||||
|
||||
private:
|
||||
void rebuildChips(const std::vector<Entry>& entries);
|
||||
|
||||
ItemIconCache* m_itemIcons;
|
||||
QGridLayout* m_layout;
|
||||
std::vector<std::string> m_itemIds; // what the chips currently stand for
|
||||
std::vector<ItemChip*> m_chips;
|
||||
};
|
||||
99
src/ui/selection/MultiBuildingContent.cpp
Normal file
99
src/ui/selection/MultiBuildingContent.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "MultiBuildingContent.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingIconCache.h"
|
||||
#include "ClearBeltControl.h"
|
||||
#include "CountRow.h"
|
||||
#include "FactoryQueries.h"
|
||||
#include "GameConfig.h"
|
||||
#include "SelectionNames.h"
|
||||
#include "Simulation.h"
|
||||
#include "StatRow.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Size the type symbol is drawn at on a count row, matching the card header's chip.
|
||||
const int kCountSymbolSizePx = 20;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
MultiBuildingContent::MultiBuildingContent(const SelectionContext& context,
|
||||
const SelectionRequest& request,
|
||||
QWidget* parent)
|
||||
: SelectionContent(context, std::nullopt, parent)
|
||||
, m_ids(request.buildings)
|
||||
{
|
||||
// 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();
|
||||
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
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]++;
|
||||
}
|
||||
}
|
||||
|
||||
int totalCost = 0;
|
||||
for (const std::pair<const BuildingType, int>& entry : counts)
|
||||
{
|
||||
getRuntimeLayout()->addWidget(new CountRow(
|
||||
getContext().buildingIcons->getChip(buildingTypeId(entry.first),
|
||||
kCountSymbolSizePx),
|
||||
getBuildingTypeName(entry.first), entry.second, this));
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
StatRow* totalRow = new StatRow(tr("Total cost"), this);
|
||||
totalRow->setValue(QString::number(totalCost));
|
||||
totalRow->setValueEmphasized(true);
|
||||
getRuntimeLayout()->addWidget(totalRow);
|
||||
}
|
||||
31
src/ui/selection/MultiBuildingContent.h
Normal file
31
src/ui/selection/MultiBuildingContent.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "BuildingId.h"
|
||||
#include "SelectionContent.h"
|
||||
#include "SelectionContentFactory.h"
|
||||
|
||||
|
||||
// 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;
|
||||
};
|
||||
48
src/ui/selection/ProductionSection.cpp
Normal file
48
src/ui/selection/ProductionSection.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "ProductionSection.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BarRow.h"
|
||||
#include "Building.h"
|
||||
#include "SectionBox.h"
|
||||
|
||||
ProductionSection::ProductionSection(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
m_section = new SectionBox(tr("Production"), this);
|
||||
m_bar = new BarRow(QString(), m_section);
|
||||
m_section->getContentLayout()->addWidget(m_bar);
|
||||
layout->addWidget(m_section);
|
||||
}
|
||||
|
||||
void ProductionSection::setProduction(bool runsProduction, const Building& building,
|
||||
double durationSeconds, Tick currentTick)
|
||||
{
|
||||
// Nothing selected to produce means no progress indicator at all
|
||||
// (REQ-UI-PRODUCTION-PROGRESS).
|
||||
if (!runsProduction)
|
||||
{
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
show();
|
||||
|
||||
// No running cycle, or none whose length is known: the bar is empty and reads idle.
|
||||
if (durationSeconds <= 0.0 || !building.production.has_value())
|
||||
{
|
||||
m_bar->setValue(0.0, tr("idle"));
|
||||
return;
|
||||
}
|
||||
|
||||
const Tick cycleTicks = secondsToTicks(durationSeconds);
|
||||
const Tick elapsed = currentTick - (building.production->completesAt - cycleTicks);
|
||||
const Tick clamped = std::max(Tick(0), std::min(cycleTicks, elapsed));
|
||||
const int percent = static_cast<int>(clamped * 100 / cycleTicks);
|
||||
m_bar->setValue(static_cast<double>(clamped) / cycleTicks, tr("%1%").arg(percent));
|
||||
}
|
||||
35
src/ui/selection/ProductionSection.h
Normal file
35
src/ui/selection/ProductionSection.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "Tick.h"
|
||||
|
||||
struct Building;
|
||||
class BarRow;
|
||||
class SectionBox;
|
||||
|
||||
// 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:
|
||||
SectionBox* m_section;
|
||||
BarRow* m_bar;
|
||||
};
|
||||
51
src/ui/selection/RecipeProductionContent.cpp
Normal file
51
src/ui/selection/RecipeProductionContent.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "RecipeProductionContent.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BuildingTarget.h"
|
||||
#include "GameConfig.h"
|
||||
#include "RecipeSelectionControl.h"
|
||||
|
||||
RecipeProductionContent::RecipeProductionContent(const SelectionContext& context,
|
||||
const SelectionRequest& request,
|
||||
QWidget* parent)
|
||||
: BufferedBuildingContent(context, request.buildings.front(), parent)
|
||||
{
|
||||
const BuildingTarget target = resolveBuildingTarget(context, getBuildingId());
|
||||
|
||||
// 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()->insertWidget(0, m_recipeControl);
|
||||
}
|
||||
|
||||
void RecipeProductionContent::refreshControls(const BuildingTarget& target)
|
||||
{
|
||||
m_recipeControl->setRecipeId(target.recipeId);
|
||||
}
|
||||
|
||||
BufferedBuildingContent::CycleInfo RecipeProductionContent::getCycleInfo(
|
||||
const BuildingTarget& target) const
|
||||
{
|
||||
CycleInfo info;
|
||||
const RecipeDef* recipe = target.recipeId.empty()
|
||||
? nullptr
|
||||
: getContext().config->recipes.findRecipeDef(target.recipeId, target.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 refreshControls(const BuildingTarget& target) override;
|
||||
CycleInfo getCycleInfo(const BuildingTarget& target) 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;
|
||||
};
|
||||
108
src/ui/selection/RecipeSummaryRow.cpp
Normal file
108
src/ui/selection/RecipeSummaryRow.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#include "RecipeSummaryRow.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLayoutItem>
|
||||
#include <QPixmap>
|
||||
|
||||
#include "ItemIconCache.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Size the item icons are drawn at on the summary line, in device-independent pixels.
|
||||
const int kSummaryIconSizePx = 14;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
RecipeSummaryRow::RecipeSummaryRow(ItemIconCache* itemIcons, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_itemIcons(itemIcons)
|
||||
{
|
||||
m_layout = new QHBoxLayout(this);
|
||||
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout->setSpacing(4);
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
void RecipeSummaryRow::setSummary(const std::vector<Amount>& inputs,
|
||||
const std::vector<Amount>& outputs,
|
||||
double durationSeconds)
|
||||
{
|
||||
if (outputs.empty() && inputs.empty())
|
||||
{
|
||||
// Forgotten as well as hidden, so re-selecting the same recipe later is seen as
|
||||
// a change and shows the row again.
|
||||
m_inputs.clear();
|
||||
m_outputs.clear();
|
||||
m_durationSeconds = -1.0;
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
if (inputs == m_inputs && outputs == m_outputs
|
||||
&& durationSeconds == m_durationSeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_inputs = inputs;
|
||||
m_outputs = outputs;
|
||||
m_durationSeconds = durationSeconds;
|
||||
rebuild(inputs, outputs, durationSeconds);
|
||||
show();
|
||||
}
|
||||
|
||||
void RecipeSummaryRow::rebuild(const std::vector<Amount>& inputs,
|
||||
const std::vector<Amount>& outputs,
|
||||
double durationSeconds)
|
||||
{
|
||||
while (QLayoutItem* item = m_layout->takeAt(0))
|
||||
{
|
||||
if (item->widget())
|
||||
{
|
||||
item->widget()->deleteLater();
|
||||
}
|
||||
delete item;
|
||||
}
|
||||
|
||||
addAmounts(inputs);
|
||||
if (!inputs.empty())
|
||||
{
|
||||
const QChar rightArrow(0x2192); // U+2192 RIGHTWARDS ARROW
|
||||
m_layout->addWidget(new QLabel(QString(rightArrow), this));
|
||||
}
|
||||
addAmounts(outputs);
|
||||
|
||||
if (durationSeconds > 0.0)
|
||||
{
|
||||
const QChar middleDot(0x00B7); // U+00B7 MIDDLE DOT
|
||||
m_layout->addWidget(new QLabel(
|
||||
QStringLiteral("%1 %2").arg(middleDot).arg(
|
||||
tr("%1 s").arg(durationSeconds, 0, 'f', 1)), this));
|
||||
}
|
||||
m_layout->addStretch(1);
|
||||
}
|
||||
|
||||
void RecipeSummaryRow::addAmounts(const std::vector<Amount>& amounts)
|
||||
{
|
||||
for (const Amount& entry : amounts)
|
||||
{
|
||||
// A missing icon file is not an error (REQ-UI-ITEM-ICON): the item's id then
|
||||
// stands in for its icon.
|
||||
if (m_itemIcons->hasIcon(entry.itemId))
|
||||
{
|
||||
QLabel* iconLabel = new QLabel(this);
|
||||
iconLabel->setPixmap(
|
||||
m_itemIcons->getPixmap(entry.itemId, kSummaryIconSizePx));
|
||||
m_layout->addWidget(iconLabel);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_layout->addWidget(
|
||||
new QLabel(QString::fromStdString(entry.itemId), this));
|
||||
}
|
||||
m_layout->addWidget(new QLabel(QString::number(entry.amount), this));
|
||||
}
|
||||
}
|
||||
52
src/ui/selection/RecipeSummaryRow.h
Normal file
52
src/ui/selection/RecipeSummaryRow.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class ItemIconCache;
|
||||
class QHBoxLayout;
|
||||
|
||||
// What one production cycle does, on a line: each input item with its per-cycle amount,
|
||||
// an arrow, each output with its amount, and the cycle time
|
||||
// (REQ-UI-RECIPE-SUMMARY). It restates the selected recipe without the player having to
|
||||
// open the selection dialog, and it is the panel's only display of the cycle time.
|
||||
class RecipeSummaryRow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct Amount
|
||||
{
|
||||
std::string itemId;
|
||||
int amount = 0;
|
||||
|
||||
bool operator==(const Amount& other) const
|
||||
{
|
||||
return itemId == other.itemId && amount == other.amount;
|
||||
}
|
||||
};
|
||||
|
||||
// itemIcons is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); an item with
|
||||
// no icon file falls back to its id. Not owned.
|
||||
explicit RecipeSummaryRow(ItemIconCache* itemIcons, QWidget* parent = nullptr);
|
||||
|
||||
// Hides the row when there is nothing selected to produce, which is how a building
|
||||
// with no recipe shows no summary at all.
|
||||
void setSummary(const std::vector<Amount>& inputs,
|
||||
const std::vector<Amount>& outputs, double durationSeconds);
|
||||
|
||||
private:
|
||||
void rebuild(const std::vector<Amount>& inputs, const std::vector<Amount>& outputs,
|
||||
double durationSeconds);
|
||||
void addAmounts(const std::vector<Amount>& amounts);
|
||||
|
||||
ItemIconCache* m_itemIcons;
|
||||
QHBoxLayout* m_layout;
|
||||
// What the row currently shows, so a refresh at tick rate rebuilds it only when the
|
||||
// recipe actually changed.
|
||||
std::vector<Amount> m_inputs;
|
||||
std::vector<Amount> m_outputs;
|
||||
double m_durationSeconds = -1.0;
|
||||
};
|
||||
50
src/ui/selection/SectionBox.cpp
Normal file
50
src/ui/selection/SectionBox.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "SectionBox.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QLabel>
|
||||
#include <QPalette>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Point-size drop of the caption relative to the card's text, so it reads as a heading
|
||||
// without competing with the values beneath it.
|
||||
const int kCaptionSizeDropPt = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
SectionBox::SectionBox(const QString& caption, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(2);
|
||||
|
||||
m_captionLabel = new QLabel(caption.toUpper(), this);
|
||||
QFont captionFont = m_captionLabel->font();
|
||||
captionFont.setPointSize(qMax(1, captionFont.pointSize() - kCaptionSizeDropPt));
|
||||
captionFont.setBold(true);
|
||||
m_captionLabel->setFont(captionFont);
|
||||
|
||||
// Dimmed to the palette's disabled text, so the caption sits behind its content.
|
||||
QPalette captionPalette = m_captionLabel->palette();
|
||||
captionPalette.setColor(QPalette::WindowText,
|
||||
palette().color(QPalette::Disabled, QPalette::WindowText));
|
||||
m_captionLabel->setPalette(captionPalette);
|
||||
m_captionLabel->setVisible(!caption.isEmpty());
|
||||
|
||||
QWidget* content = new QWidget(this);
|
||||
m_contentLayout = new QVBoxLayout(content);
|
||||
m_contentLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_contentLayout->setSpacing(2);
|
||||
|
||||
layout->addWidget(m_captionLabel);
|
||||
layout->addWidget(content);
|
||||
}
|
||||
|
||||
QVBoxLayout* SectionBox::getContentLayout()
|
||||
{
|
||||
return m_contentLayout;
|
||||
}
|
||||
28
src/ui/selection/SectionBox.h
Normal file
28
src/ui/selection/SectionBox.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
class QVBoxLayout;
|
||||
|
||||
// A captioned group of parts within a card's configuration or runtime group
|
||||
// (REQ-UI-SELECTION-CARD): a small heading over whatever the owner puts inside it.
|
||||
//
|
||||
// A section and its caption are shown or hidden as one, which is what lets a card list
|
||||
// its sections unconditionally and simply hide the ones with nothing in them -- a Miner
|
||||
// consumes nothing, so its input buffer section is never shown.
|
||||
class SectionBox : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SectionBox(const QString& caption, QWidget* parent = nullptr);
|
||||
|
||||
// The layout to add this section's parts to.
|
||||
QVBoxLayout* getContentLayout();
|
||||
|
||||
private:
|
||||
QLabel* m_captionLabel;
|
||||
QVBoxLayout* m_contentLayout;
|
||||
};
|
||||
222
src/ui/selection/SelectionContent.cpp
Normal file
222
src/ui/selection/SelectionContent.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include "SelectionContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include <QFont>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BarRow.h"
|
||||
#include "BuildingIconCache.h"
|
||||
#include "EmptyNote.h"
|
||||
#include "FactoryQueries.h"
|
||||
#include "GameConfig.h"
|
||||
#include "ProductionRules.h"
|
||||
#include "SectionBox.h"
|
||||
#include "Simulation.h"
|
||||
#include "StatusPill.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;
|
||||
|
||||
// Spacing inside the card and between the header's elements.
|
||||
const int kCardSpacingPx = 6;
|
||||
const int kHeaderSpacingPx = 6;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
SelectionContent::SelectionContent(const SelectionContext& context,
|
||||
std::optional<BuildingId> constructionSiteId,
|
||||
QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_context(context)
|
||||
, m_siteId(constructionSiteId)
|
||||
, m_constructionSection(nullptr)
|
||||
, m_constructionBar(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_statusPill = new StatusPill(header);
|
||||
|
||||
headerLayout->addWidget(m_symbolLabel);
|
||||
headerLayout->addWidget(m_nameLabel);
|
||||
headerLayout->addStretch(1);
|
||||
headerLayout->addWidget(m_statusPill);
|
||||
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_constructionSection = new SectionBox(tr("Construction"), this);
|
||||
m_constructionBar = new BarRow(QString(), m_constructionSection);
|
||||
m_constructionSection->getContentLayout()->addWidget(m_constructionBar);
|
||||
m_constructionSection->getContentLayout()->addWidget(
|
||||
new EmptyNote(tr("No buffers until built"), m_constructionSection));
|
||||
cardLayout->addWidget(m_constructionSection);
|
||||
|
||||
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)
|
||||
{
|
||||
m_statusPill->setStatus(dotColor, m_context.visuals->statusLight.outline, caption);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (site->completesAt == 0)
|
||||
{
|
||||
// Placed but not yet at the head of the construction queue (REQ-BLD-QUEUE), so
|
||||
// there is no progress to show yet.
|
||||
m_constructionBar->setValue(0.0, tr("Queued"));
|
||||
return;
|
||||
}
|
||||
|
||||
const BuildingDef* def = m_context.config->buildings.findBuildingDef(site->type);
|
||||
if (!def || def->constructionTimeSeconds <= 0)
|
||||
{
|
||||
m_constructionBar->setValue(0.0, tr("Building..."));
|
||||
return;
|
||||
}
|
||||
|
||||
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
|
||||
const Tick elapsed =
|
||||
m_context.sim->getCurrentTick() - (site->completesAt - duration);
|
||||
const Tick clamped = std::max(Tick(0), std::min(duration, elapsed));
|
||||
const int percent = static_cast<int>(clamped * 100 / duration);
|
||||
m_constructionBar->setValue(static_cast<double>(clamped) / duration,
|
||||
tr("%1%").arg(percent));
|
||||
}
|
||||
104
src/ui/selection/SelectionContent.h
Normal file
104
src/ui/selection/SelectionContent.h
Normal file
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QColor>
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include "BuildingId.h"
|
||||
#include "BuildingType.h"
|
||||
#include "SelectionContext.h"
|
||||
|
||||
struct Building;
|
||||
class BarRow;
|
||||
class QLabel;
|
||||
class QVBoxLayout;
|
||||
class SectionBox;
|
||||
class StatusPill;
|
||||
|
||||
// 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;
|
||||
StatusPill* m_statusPill;
|
||||
|
||||
QWidget* m_configurationGroup;
|
||||
QWidget* m_runtimeGroup;
|
||||
// Replaces the runtime group while this card shows a construction site; both null
|
||||
// otherwise.
|
||||
SectionBox* m_constructionSection;
|
||||
BarRow* m_constructionBar;
|
||||
};
|
||||
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;
|
||||
};
|
||||
35
src/ui/selection/SelectionNames.cpp
Normal file
35
src/ui/selection/SelectionNames.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#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)));
|
||||
}
|
||||
|
||||
QString getBehaviorLabel(BehaviorKind kind)
|
||||
{
|
||||
// Only the winning behavior is named; the salvage and repair cycles that run
|
||||
// regardless of it are not behaviors here (REQ-UI-SHIP-BEHAVIOR).
|
||||
switch (kind)
|
||||
{
|
||||
case BehaviorKind::Retreat: return QObject::tr("Retreating");
|
||||
case BehaviorKind::Attack: return QObject::tr("Engaging");
|
||||
case BehaviorKind::SalvageScrap:
|
||||
case BehaviorKind::DeliverScrap: return QObject::tr("Salvaging");
|
||||
case BehaviorKind::Repair: return QObject::tr("Repairing");
|
||||
case BehaviorKind::Rally: return QObject::tr("Rallying");
|
||||
case BehaviorKind::Standby: return QObject::tr("Standby");
|
||||
case BehaviorKind::Advance: return QObject::tr("Advancing");
|
||||
case BehaviorKind::None: break;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
15
src/ui/selection/SelectionNames.h
Normal file
15
src/ui/selection/SelectionNames.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "BehaviorKind.h"
|
||||
#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);
|
||||
|
||||
// Name of the behavior currently governing a ship, for the ship card's header slot
|
||||
// (REQ-UI-SHIP-BEHAVIOR). Empty when no behavior has won yet, which shows no slot.
|
||||
QString getBehaviorLabel(BehaviorKind kind);
|
||||
67
src/ui/selection/ShipContent.cpp
Normal file
67
src/ui/selection/ShipContent.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "ShipContent.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "EntityAdmin.h"
|
||||
#include "GameConfig.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "SelectedBehaviorComponent.h"
|
||||
#include "SelectionNames.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->setDebugDrawEnabled(*getContext().debugDrawEnabled);
|
||||
|
||||
// The behavior is the header's right slot rather than a stat row, so it reads as
|
||||
// what the ship is doing rather than as another number (REQ-UI-SHIP-BEHAVIOR).
|
||||
setSlot(QColor(), getBehaviorLabel(
|
||||
admin.get<SelectedBehaviorComponent>(m_entity).winner));
|
||||
|
||||
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;
|
||||
};
|
||||
102
src/ui/selection/ShipyardContent.cpp
Normal file
102
src/ui/selection/ShipyardContent.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include "ShipyardContent.h"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BuildingTarget.h"
|
||||
#include "EventManager.h"
|
||||
#include "GameConfig.h"
|
||||
#include "LayoutDialogRequestedEvent.h"
|
||||
#include "ProductionRules.h"
|
||||
#include "RecipeSelectionControl.h"
|
||||
#include "SectionBox.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_layoutSection = new SectionBox(tr("Layout"), this);
|
||||
m_layoutPreview = new ShipLayoutPreview(m_layoutSection);
|
||||
m_configureButton = new QPushButton(tr("Configure Layout"), m_layoutSection);
|
||||
m_layoutSection->getContentLayout()->addWidget(m_layoutPreview);
|
||||
m_layoutSection->getContentLayout()->addWidget(m_configureButton);
|
||||
|
||||
// Ahead of the recipe summary the base adds, so the card reads schematic, layout,
|
||||
// then what one ship costs.
|
||||
getConfigurationLayout()->insertWidget(0, m_schematicControl);
|
||||
getConfigurationLayout()->insertWidget(1, m_layoutSection);
|
||||
|
||||
const BuildingId id = getBuildingId();
|
||||
connect(m_configureButton, &QPushButton::clicked, this, [id]() {
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<LayoutDialogRequestedEvent>(id));
|
||||
});
|
||||
}
|
||||
|
||||
void ShipyardContent::refreshControls(const BuildingTarget& target)
|
||||
{
|
||||
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 BuildingTarget& target) const
|
||||
{
|
||||
CycleInfo info;
|
||||
const ShipDef* shipDef = target.recipeId.empty()
|
||||
? nullptr
|
||||
: getContext().config->ships.findShipDef(target.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, target.recipeId, target.shipLayout);
|
||||
|
||||
// A shipyard's output is the ship itself, which never lands in an item buffer -- so
|
||||
// the summary shows one ship rather than an item id.
|
||||
info.durationSeconds = shipDef->schematic.productionTimeSeconds;
|
||||
if (target.shipLayout.has_value())
|
||||
{
|
||||
for (const PlacedModule& placed : target.shipLayout->placedModules)
|
||||
{
|
||||
const ModuleDef* moduleDef =
|
||||
getContext().config->modules.findModuleDef(placed.moduleId);
|
||||
if (moduleDef)
|
||||
{
|
||||
info.durationSeconds += moduleDef->productionTimeSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
info.runsProduction = true;
|
||||
return info;
|
||||
}
|
||||
34
src/ui/selection/ShipyardContent.h
Normal file
34
src/ui/selection/ShipyardContent.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "BufferedBuildingContent.h"
|
||||
#include "SelectionContentFactory.h"
|
||||
|
||||
class QPushButton;
|
||||
class RecipeSelectionControl;
|
||||
class SectionBox;
|
||||
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 refreshControls(const BuildingTarget& target) override;
|
||||
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
||||
|
||||
private:
|
||||
RecipeSelectionControl* m_schematicControl;
|
||||
SectionBox* m_layoutSection;
|
||||
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;
|
||||
};
|
||||
54
src/ui/selection/StatRow.cpp
Normal file
54
src/ui/selection/StatRow.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "StatRow.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPalette>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Left inset of an indented row, in device-independent pixels.
|
||||
const int kIndentPx = 12;
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
StatRow::StatRow(const QString& label, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(8);
|
||||
|
||||
m_labelLabel = new QLabel(label, this);
|
||||
m_valueLabel = new QLabel(this);
|
||||
m_valueLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
|
||||
layout->addWidget(m_labelLabel);
|
||||
layout->addStretch(1);
|
||||
layout->addWidget(m_valueLabel);
|
||||
}
|
||||
|
||||
void StatRow::setLabel(const QString& label)
|
||||
{
|
||||
m_labelLabel->setText(label);
|
||||
}
|
||||
|
||||
void StatRow::setValue(const QString& value)
|
||||
{
|
||||
m_valueLabel->setText(value);
|
||||
}
|
||||
|
||||
void StatRow::setValueEmphasized(bool emphasized)
|
||||
{
|
||||
QPalette valuePalette = m_valueLabel->palette();
|
||||
valuePalette.setColor(QPalette::WindowText,
|
||||
palette().color(emphasized ? QPalette::Highlight
|
||||
: QPalette::WindowText));
|
||||
m_valueLabel->setPalette(valuePalette);
|
||||
}
|
||||
|
||||
void StatRow::setIndented(bool indented)
|
||||
{
|
||||
layout()->setContentsMargins(indented ? kIndentPx : 0, 0, 0, 0);
|
||||
}
|
||||
33
src/ui/selection/StatRow.h
Normal file
33
src/ui/selection/StatRow.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
|
||||
// One label/value line: the name on the left, the value hard right
|
||||
// (REQ-UI-SELECTION-CARD). The panel's plainest part, shared by the ship and station
|
||||
// stats, the debris scrap, the HQ block stock, and the multi-selection's total cost.
|
||||
//
|
||||
// Like the other card parts it knows nothing about the simulation: it is given the two
|
||||
// strings and lays them out.
|
||||
class StatRow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StatRow(const QString& label, QWidget* parent = nullptr);
|
||||
|
||||
void setLabel(const QString& label);
|
||||
void setValue(const QString& value);
|
||||
// Draws the value in the palette's highlight color rather than its text color, for
|
||||
// the one value a card is really about.
|
||||
void setValueEmphasized(bool emphasized);
|
||||
// Indents the row, so it reads as belonging to the row above it -- the scrap total
|
||||
// under a debris count (REQ-UI-FIELD-MULTI-SELECTION).
|
||||
void setIndented(bool indented);
|
||||
|
||||
private:
|
||||
QLabel* m_labelLabel;
|
||||
QLabel* m_valueLabel;
|
||||
};
|
||||
83
src/ui/selection/StationContent.cpp
Normal file
83
src/ui/selection/StationContent.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "StationContent.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BarRow.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "ModuleOwnerComponent.h"
|
||||
#include "Simulation.h"
|
||||
#include "StatRow.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_hpBar = new BarRow(tr("HP"), this);
|
||||
m_damageRow = new StatRow(tr("Damage"), this);
|
||||
m_rangeRow = new StatRow(tr("Range"), this);
|
||||
m_fireRateRow = new StatRow(tr("Fire rate"), this);
|
||||
|
||||
getRuntimeLayout()->addWidget(m_hpBar);
|
||||
getRuntimeLayout()->addWidget(m_damageRow);
|
||||
getRuntimeLayout()->addWidget(m_rangeRow);
|
||||
getRuntimeLayout()->addWidget(m_fireRateRow);
|
||||
|
||||
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);
|
||||
|
||||
const double hpFraction = (health.maxHp > 0.0f)
|
||||
? static_cast<double>(health.hp) / health.maxHp
|
||||
: 0.0;
|
||||
m_hpBar->setValue(hpFraction, tr("%1 / %2")
|
||||
.arg(static_cast<int>(health.hp + 0.5f))
|
||||
.arg(static_cast<int>(health.maxHp + 0.5f)));
|
||||
|
||||
// A station's weapons are child module entities pointing back at it, so its damage,
|
||||
// range and fire rate are read off those rather than off the station itself.
|
||||
float totalDamage = 0.0f;
|
||||
float maxRange = 0.0f;
|
||||
float maxFireRateHz = 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;
|
||||
totalDamage += weapon.damage;
|
||||
if (weapon.range_tiles > maxRange) { maxRange = weapon.range_tiles; }
|
||||
if (weapon.fireRateHz > maxFireRateHz) { maxFireRateHz = weapon.fireRateHz; }
|
||||
});
|
||||
|
||||
m_damageRow->setVisible(hasWeapon);
|
||||
m_rangeRow->setVisible(hasWeapon);
|
||||
m_fireRateRow->setVisible(hasWeapon);
|
||||
if (hasWeapon)
|
||||
{
|
||||
m_damageRow->setValue(
|
||||
QString::number(static_cast<double>(totalDamage), 'f', 1));
|
||||
m_rangeRow->setValue(tr("%1 tiles")
|
||||
.arg(QString::number(static_cast<double>(maxRange), 'f', 1)));
|
||||
m_fireRateRow->setValue(tr("%1 /s")
|
||||
.arg(QString::number(static_cast<double>(maxFireRateHz), 'f', 1)));
|
||||
}
|
||||
}
|
||||
31
src/ui/selection/StationContent.h
Normal file
31
src/ui/selection/StationContent.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
|
||||
#include "SelectionContent.h"
|
||||
#include "SelectionContentFactory.h"
|
||||
|
||||
class BarRow;
|
||||
class StatRow;
|
||||
|
||||
// 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;
|
||||
BarRow* m_hpBar;
|
||||
StatRow* m_damageRow;
|
||||
StatRow* m_rangeRow;
|
||||
StatRow* m_fireRateRow;
|
||||
};
|
||||
68
src/ui/selection/StatusPill.cpp
Normal file
68
src/ui/selection/StatusPill.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "StatusPill.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QRectF>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Diameter of the status dot, in device-independent pixels.
|
||||
const int kDotSizePx = 8;
|
||||
|
||||
QPixmap renderDot(const QColor& fill, const QColor& outline)
|
||||
{
|
||||
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||
QPixmap pixmap(static_cast<int>(kDotSizePx * dpr),
|
||||
static_cast<int>(kDotSizePx * dpr));
|
||||
pixmap.setDevicePixelRatio(dpr);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
painter.setPen(outline.isValid() ? QPen(outline) : QPen(Qt::NoPen));
|
||||
painter.setBrush(fill);
|
||||
// Inset by half the pen width so the outline stays inside the pixmap.
|
||||
painter.drawEllipse(QRectF(0.5, 0.5, kDotSizePx - 1.0, kDotSizePx - 1.0));
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
StatusPill::StatusPill(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(4);
|
||||
|
||||
m_dotLabel = new QLabel(this);
|
||||
m_captionLabel = new QLabel(this);
|
||||
|
||||
layout->addWidget(m_dotLabel);
|
||||
layout->addWidget(m_captionLabel);
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
void StatusPill::setStatus(const QColor& dotColor, const QColor& outlineColor,
|
||||
const QString& caption)
|
||||
{
|
||||
if (dotColor.isValid())
|
||||
{
|
||||
m_dotLabel->setPixmap(renderDot(dotColor, outlineColor));
|
||||
m_dotLabel->show();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_dotLabel->hide();
|
||||
}
|
||||
|
||||
m_captionLabel->setText(caption);
|
||||
m_captionLabel->setVisible(!caption.isEmpty());
|
||||
setVisible(dotColor.isValid() || !caption.isEmpty());
|
||||
}
|
||||
29
src/ui/selection/StatusPill.h
Normal file
29
src/ui/selection/StatusPill.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
class QLabel;
|
||||
|
||||
// The card header's right slot (REQ-UI-SELECTION-CARD): a colored dot with a short
|
||||
// caption beside it. It carries a building's production status
|
||||
// (REQ-UI-SELECTION-STATUS), a ship's current behavior (REQ-UI-SHIP-BEHAVIOR), or an
|
||||
// aggregated selection's object count (REQ-UI-SELECTION-AGGREGATE) -- never more than
|
||||
// one of them, which is why they share one part.
|
||||
class StatusPill : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StatusPill(QWidget* parent = nullptr);
|
||||
|
||||
// An invalid dot color leaves the dot off, for the slots that are a plain caption.
|
||||
// An empty caption hides the pill entirely.
|
||||
void setStatus(const QColor& dotColor, const QColor& outlineColor,
|
||||
const QString& caption);
|
||||
|
||||
private:
|
||||
QLabel* m_dotLabel;
|
||||
QLabel* m_captionLabel;
|
||||
};
|
||||
17
src/ui/selection/StorageContent.cpp
Normal file
17
src/ui/selection/StorageContent.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "StorageContent.h"
|
||||
|
||||
#include "BuildingTarget.h"
|
||||
|
||||
StorageContent::StorageContent(const SelectionContext& context,
|
||||
const SelectionRequest& request, QWidget* parent)
|
||||
: BufferedBuildingContent(context, request.buildings.front(), parent)
|
||||
{
|
||||
}
|
||||
|
||||
BufferedBuildingContent::CycleInfo StorageContent::getCycleInfo(
|
||||
const BuildingTarget& /*target*/) const
|
||||
{
|
||||
// No recipe, no cycle: the card shows the output buffer alone, with no per-cycle
|
||||
// denominators and no production progress (REQ-BLD-SALVAGE-BAY).
|
||||
return CycleInfo();
|
||||
}
|
||||
21
src/ui/selection/StorageContent.h
Normal file
21
src/ui/selection/StorageContent.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#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:
|
||||
CycleInfo getCycleInfo(const BuildingTarget& target) const override;
|
||||
};
|
||||
Reference in New Issue
Block a user