Rename Demolish to Deconstruct; add build button grid icons

Rename the Demolish* terminology to Deconstruct* across the simulation,
events, UI, tests, and docs (DemolishCommand and the DemolishMode* events
become their Deconstruct* equivalents), aligning with the deconstruction
queue at HEAD.

Add per-building SVG icons for the build button grid (REQ-UI-BUILD-GRID):
one color-chip icon per placeable building under data/icons/buildings,
loaded from disk at runtime and rendered with QtSvg onto each build button.
An unaffordable building's button shows a grey-recolored variant via a
QIcon Disabled-mode pixmap (REQ-UI-BUILD-DISABLED). Adds the Qt5::Svg
dependency and deploys Qt5Svg.dll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
2026-07-22 18:38:18 +02:00
parent 1914705ab9
commit ae41f677e6
43 changed files with 310 additions and 170 deletions

View File

@@ -2,23 +2,81 @@
#include <string>
#include <QByteArray>
#include <QFile>
#include <QGridLayout>
#include <QIcon>
#include <QPainter>
#include <QPixmap>
#include <QPushButton>
#include <QRegularExpression>
#include <QSignalMapper>
#include <QSize>
#include <QString>
#include <QSvgRenderer>
#include "BuildingType.h"
#include "BuildingTypeSelectedEvent.h"
#include "DemolishModeToggleRequestedEvent.h"
#include "DeconstructModeToggleRequestedEvent.h"
#include "DisplayName.h"
#include "EventManager.h"
#include "ExitBuilderModeRequestedEvent.h"
#include "Simulation.h"
namespace
{
// Pixel size the SVG chips are rasterized at; downscaled to the button icon size.
const int kIconRenderSize = 64;
const QSize kIconSize(28, 28);
BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config, QWidget* parent)
QPixmap renderChip(const QByteArray& svg)
{
QSvgRenderer renderer(svg);
QPixmap pixmap(kIconRenderSize, kIconRenderSize);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
renderer.render(&painter);
return pixmap;
}
// Builds a build-button icon from a "<id>.svg" chip file. The returned QIcon also
// carries a Disabled-mode pixmap whose chip background is recolored grey, so an
// unaffordable (disabled) button shows the grey variant automatically
// (REQ-UI-BUILD-DISABLED) without any extra work in updateAffordability().
QIcon loadBuildingIcon(const QString& path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly))
{
return QIcon();
}
const QByteArray svg = file.readAll();
QIcon icon;
icon.addPixmap(renderChip(svg), QIcon::Normal);
// 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\""));
}
icon.addPixmap(renderChip(greyed.toUtf8()), QIcon::Disabled);
return icon;
}
}
BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config,
const std::string& iconDir, QWidget* parent)
: QWidget(parent)
, m_sim(sim)
, m_config(config)
, m_iconDir(iconDir)
{
QGridLayout* layout = new QGridLayout(this);
layout->setSpacing(4);
@@ -53,6 +111,12 @@ BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config, QWid
QPushButton* btn = new QPushButton(label, this);
btn->setCheckable(true);
btn->setFixedHeight(48);
// Icon file name matches the building id (REQ-UI-BUILD-GRID); Tunnel Entry's
// "tunnel_entry.svg" serves the shared Tunnel button.
const QString iconPath = QString::fromStdString(m_iconDir) + "/"
+ QString::fromStdString(def.id) + ".svg";
btn->setIcon(loadBuildingIcon(iconPath));
btn->setIconSize(kIconSize);
if (def.tooltip)
{
btn->setToolTip(QString::fromStdString(*def.tooltip));
@@ -73,13 +137,16 @@ BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config, QWid
}
connect(mapper, qOverload<int>(&QSignalMapper::mapped), this, &BuildButtonGrid::onBuildButton);
m_demolishButton = new QPushButton(tr("Demolish"), this);
m_demolishButton->setCheckable(true);
m_demolishButton->setFixedHeight(48);
layout->addWidget(m_demolishButton, row, col);
connect(m_demolishButton, &QPushButton::clicked, this, [this]() {
m_deconstructButton = new QPushButton(tr("Deconstruct"), this);
m_deconstructButton->setCheckable(true);
m_deconstructButton->setFixedHeight(48);
m_deconstructButton->setIcon(loadBuildingIcon(
QString::fromStdString(m_iconDir) + "/deconstruct.svg"));
m_deconstructButton->setIconSize(kIconSize);
layout->addWidget(m_deconstructButton, row, col);
connect(m_deconstructButton, &QPushButton::clicked, this, [this]() {
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeToggleRequestedEvent>());
std::make_shared<DeconstructModeToggleRequestedEvent>());
});
updateVisibility();
@@ -183,9 +250,9 @@ void BuildButtonGrid::handleEvent(std::shared_ptr<const UnlockedBuildingsChanged
updateAffordability();
}
void BuildButtonGrid::handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event)
void BuildButtonGrid::handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event)
{
m_demolishButton->setChecked(event->active);
m_deconstructButton->setChecked(event->active);
}
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event)