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

@@ -117,6 +117,7 @@ target_link_libraries(${TARGET_UI_NAME}
Qt5::Network
Qt5::Multimedia
Qt5::Charts
Qt5::Svg
)
target_compile_definitions(${TARGET_UI_NAME} PRIVATE TOML_FLOAT_CHARCONV=0)

View File

@@ -67,7 +67,7 @@ struct WorldScroll
struct WorldConfig
{
int heightTiles; // REQ-GW-HEIGHT
int refundPercentage; // REQ-BLD-DEMOLISH
int refundPercentage; // REQ-BLD-DECONSTRUCT
double deconstructionTimeSeconds; // REQ-BLD-DECON-QUEUE
int startingBuildingBlocks; // REQ-HQ-STARTING-BLOCKS
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP

View File

@@ -16,11 +16,11 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructModeChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTypeSelectedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildHotkeyPressedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ExitBuilderModeRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeToggleRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructModeToggleRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ExitBlueprintModeRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/TemporaryBlueprintRequestedEvent.h

View File

@@ -0,0 +1,10 @@
#pragma once
#include "Event.h"
class DeconstructModeChangedEvent : public Event
{
public:
explicit DeconstructModeChangedEvent(bool active) : active(active) {}
const bool active;
};

View File

@@ -0,0 +1,7 @@
#pragma once
#include "Event.h"
class DeconstructModeToggleRequestedEvent : public Event
{
};

View File

@@ -1,10 +0,0 @@
#pragma once
#include "Event.h"
class DemolishModeChangedEvent : public Event
{
public:
explicit DemolishModeChangedEvent(bool active) : active(active) {}
const bool active;
};

View File

@@ -1,7 +0,0 @@
#pragma once
#include "Event.h"
class DemolishModeToggleRequestedEvent : public Event
{
};

View File

@@ -52,7 +52,7 @@ public:
// tile are held and routed to one of the two outputs.
void placeSplitter(QPoint tile, Rotation outputA, Rotation outputB);
// Remove a belt or splitter tile (on demolish). Items are discarded.
// Remove a belt or splitter tile (on deconstruct). Items are discarded.
void removeTile(QPoint tile);
// -- Splitter filter configuration (REQ-BLD-SPLITTER) -------------------

View File

@@ -497,13 +497,13 @@ bool BuildingSystem::isPlacementValid(BuildingType type, QPoint anchor,
}
// ---------------------------------------------------------------------------
// Demolish
// Deconstruct
// ---------------------------------------------------------------------------
int BuildingSystem::demolish(BuildingId id, Tick currentTick)
int BuildingSystem::deconstruct(BuildingId id, Tick currentTick)
{
// Construction site? Removed instantly with the full refund; never queued
// for deconstruction (REQ-BLD-DEMOLISH).
// for deconstruction (REQ-BLD-DECONSTRUCT).
for (std::deque<ConstructionSite>::iterator it = m_constructionQueue.begin();
it != m_constructionQueue.end();
++it)
@@ -887,9 +887,9 @@ void BuildingSystem::tickDeconstruction(Tick currentTick)
return;
}
// Remove the building from the world and credit its refund (REQ-BLD-DEMOLISH).
// Remove the building from the world and credit its refund (REQ-BLD-DECONSTRUCT).
// Belt/tunnel/splitter tiles were already unregistered when the building was
// queued (see demolish), so only tile occupancy and the record remain.
// queued (see deconstruct), so only tile occupancy and the record remain.
for (std::vector<Building>::iterator it = m_buildings.begin();
it != m_buildings.end();
++it)
@@ -1632,7 +1632,7 @@ std::optional<BuildingId> BuildingSystem::findRotateInPlaceTarget(
BuildingType type, QPoint anchor, Rotation rot) const
{
// Tunnel Entries and Tunnel Exits cannot be rotated in place; re-orienting a
// tunnel requires demolishing and re-placing it (REQ-BLD-ROTATE-IN-PLACE).
// tunnel requires deconstructing and re-placing it (REQ-BLD-ROTATE-IN-PLACE).
if (type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit)
{
return std::nullopt;

View File

@@ -54,7 +54,7 @@ public:
std::function<bool(const std::string&)> isItemUnlocked,
std::mt19937& rng);
// -- Placement / demolish ------------------------------------------------
// -- Placement / deconstruct ------------------------------------------------
// Returns the new entity id, or nullopt if the placement falls outside the
// world bounds (vertical extent and asteroid left edge). Belt and Splitter
// register with BeltSystem directly; other types enter the construction
@@ -77,13 +77,13 @@ public:
// Defaults to world.regions.asteroid_width_tiles at construction.
void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }
// Mark a building or construction site for demolition (REQ-BLD-DEMOLISH).
// Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
// A construction site is removed instantly and the full cost is returned.
// A fully-built building is instead appended to the deconstruction queue
// (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
// credited later, on completion in tickDeconstruction, so this returns 0 for
// it. Returns 0 for unknown ids and for a building already queued.
int demolish(BuildingId id, Tick currentTick);
int deconstruct(BuildingId id, Tick currentTick);
// Take a building back out of the deconstruction queue before it is removed
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation

View File

@@ -27,7 +27,7 @@ class GameConfig;
enum class CommandKind
{
PlaceBuilding,
Demolish,
Deconstruct,
CancelDeconstruction,
RotateInPlace,
SetRecipe,
@@ -74,15 +74,15 @@ struct PlaceBuildingCommand : Command
};
// Marks a building for demolition: a construction site is removed instantly, a
// built building is queued for deconstruction (REQ-BLD-DEMOLISH, REQ-BLD-DECON-QUEUE).
struct DemolishCommand : Command
// built building is queued for deconstruction (REQ-BLD-DECONSTRUCT, REQ-BLD-DECON-QUEUE).
struct DeconstructCommand : Command
{
DemolishCommand() : Command(CommandKind::Demolish) {}
DeconstructCommand() : Command(CommandKind::Deconstruct) {}
std::optional<BuildingId> id;
};
// Takes a building back out of the deconstruction queue (REQ-BLD-DEMOLISH-CLICK,
// REQ-BLD-DEMOLISH-BOX). No-op if the id is not currently queued.
// Takes a building back out of the deconstruction queue (REQ-BLD-DECONSTRUCT-CLICK,
// REQ-BLD-DECONSTRUCT-BOX). No-op if the id is not currently queued.
struct CancelDeconstructionCommand : Command
{
CancelDeconstructionCommand() : Command(CommandKind::CancelDeconstruction) {}

View File

@@ -128,8 +128,8 @@ std::string serializeCommand(const Command& command)
}
break;
}
case CommandKind::Demolish:
out << "demolish " << static_cast<const DemolishCommand&>(command).id.value();
case CommandKind::Deconstruct:
out << "deconstruct " << static_cast<const DeconstructCommand&>(command).id.value();
break;
case CommandKind::CancelDeconstruction:
out << "cancel_deconstruct "
@@ -243,9 +243,9 @@ std::shared_ptr<Command> parseCommand(const std::string& tokens)
}
return c;
}
if (verb == "demolish")
if (verb == "deconstruct")
{
std::shared_ptr<DemolishCommand> c = std::make_shared<DemolishCommand>();
std::shared_ptr<DeconstructCommand> c = std::make_shared<DeconstructCommand>();
BuildingId id = 0;
if (!(in >> id)) { return nullptr; }
c->id = id;

View File

@@ -231,8 +231,8 @@ void Simulation::apply(const Command& command)
}
break;
}
case CommandKind::Demolish:
demolish(*static_cast<const DemolishCommand&>(command).id);
case CommandKind::Deconstruct:
deconstruct(*static_cast<const DeconstructCommand&>(command).id);
break;
case CommandKind::CancelDeconstruction:
cancelDeconstruction(*static_cast<const CancelDeconstructionCommand&>(command).id);
@@ -1198,9 +1198,9 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
return m_buildingSystem->place(type, anchor, rotation, m_currentTick);
}
void Simulation::demolish(BuildingId id)
void Simulation::deconstruct(BuildingId id)
{
m_buildingBlocksStock += m_buildingSystem->demolish(id, m_currentTick);
m_buildingBlocksStock += m_buildingSystem->deconstruct(id, m_currentTick);
}
void Simulation::cancelDeconstruction(BuildingId id)

View File

@@ -142,7 +142,7 @@ private:
// Marks the building with the given id for demolition: a construction site is
// removed instantly (full refund), a built building is queued for timed
// deconstruction (REQ-BLD-DECON-QUEUE), refunded on completion.
void demolish(BuildingId id);
void deconstruct(BuildingId id);
// Takes a queued building back out of the deconstruction queue (REQ-BLD-DECON-QUEUE).
void cancelDeconstruction(BuildingId id);

View File

@@ -974,7 +974,7 @@ TEST_CASE("BeltSystem: unpaired entry blocks items at front", "[belt]")
REQUIRE(bs.peekItem(Port{QPoint(3, 0), Rotation::East}).has_value());
}
TEST_CASE("BeltSystem: demolish entry discards transit items", "[belt]")
TEST_CASE("BeltSystem: deconstruct entry discards transit items", "[belt]")
{
BeltSystem bs(kFastBeltSpeed);

View File

@@ -266,7 +266,7 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
REQUIRE(bs.findSite(id) != nullptr);
}
TEST_CASE("BuildingSystem: demolishing a construction site removes it instantly with full refund",
TEST_CASE("BuildingSystem: deconstructing a construction site removes it instantly with full refund",
"[building]")
{
PlacementFixture f;
@@ -274,8 +274,8 @@ TEST_CASE("BuildingSystem: demolishing a construction site removes it instantly
f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
// Still queued for construction (not yet built): instant removal, full cost
// refunded immediately, never entering the deconstruction queue (REQ-BLD-DEMOLISH).
const int refund = f.bs.demolish(id, 0);
// refunded immediately, never entering the deconstruction queue (REQ-BLD-DECONSTRUCT).
const int refund = f.bs.deconstruct(id, 0);
REQUIRE(refund == 15); // Miner cost = 15
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
@@ -366,7 +366,7 @@ static void runUntilBuilt(PlacementFixture& f, BuildingId id, Tick& tick)
REQUIRE(f.bs.findBuilding(id) != nullptr);
}
TEST_CASE("BuildingSystem: demolishing a built building queues it; refund credited on completion",
TEST_CASE("BuildingSystem: deconstructing a built building queues it; refund credited on completion",
"[building][decon]")
{
PlacementFixture f;
@@ -376,9 +376,9 @@ TEST_CASE("BuildingSystem: demolishing a built building queues it; refund credit
Tick tick = 0;
runUntilBuilt(f, id, tick);
// Demolishing a built building returns nothing immediately and queues it,
// Deconstructing a built building returns nothing immediately and queues it,
// stopping it operating while its tiles stay occupied (REQ-BLD-DECON-QUEUE).
const int refund = f.bs.demolish(id, tick);
const int refund = f.bs.deconstruct(id, tick);
REQUIRE(refund == 0);
REQUIRE(f.bs.isQueuedForDeconstruction(id));
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
@@ -403,8 +403,8 @@ TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time",
runUntilBuilt(f, b, tick);
// Queue both in one tick; 'a' is at the front of the deconstruction queue.
f.bs.demolish(a, tick);
f.bs.demolish(b, tick);
f.bs.deconstruct(a, tick);
f.bs.deconstruct(b, tick);
REQUIRE(f.bs.isQueuedForDeconstruction(a));
REQUIRE(f.bs.isQueuedForDeconstruction(b));
@@ -432,7 +432,7 @@ TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with n
Tick tick = 0;
runUntilBuilt(f, id, tick);
f.bs.demolish(id, tick);
f.bs.deconstruct(id, tick);
REQUIRE(f.bs.isQueuedForDeconstruction(id));
// Un-queue before it drains: it operates again, no refund, tiles still occupied.
@@ -460,7 +460,7 @@ TEST_CASE("BuildingSystem: queued belt stops transporting; cancel restores it",
// Queuing a belt unregisters its tile from the belt subsystem, so it no longer
// accepts or transports items, though the tile stays occupied (REQ-BLD-DECON-QUEUE).
f.bs.demolish(id, tick);
f.bs.deconstruct(id, tick);
REQUIRE_FALSE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East));
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
@@ -482,7 +482,7 @@ TEST_CASE("BuildingSystem: splitter filters survive a queue/un-queue round-trip"
// Queue: the belt subsystem tile (and its filters) are unregistered, but the
// filters are captured so an un-queue can restore them.
f.bs.demolish(id, tick);
f.bs.deconstruct(id, tick);
REQUIRE_FALSE(f.belts.getSplitterInfo(QPoint(0, 0)).has_value());
f.bs.cancelDeconstruction(id);

View File

@@ -59,7 +59,7 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe"
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
}
TEST_CASE("apply(DemolishCommand) matches direct demolish", "[command]")
TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]")
{
Simulation viaCommand(loadConfig(), 99);
Simulation viaDirect(loadConfig(), 99);
@@ -70,11 +70,11 @@ TEST_CASE("apply(DemolishCommand) matches direct demolish", "[command]")
SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
REQUIRE(idA == idB);
DemolishCommand command;
DeconstructCommand command;
command.id = idA;
viaCommand.apply(command);
SimulationTestAccess::demolish(viaDirect, idB);
SimulationTestAccess::deconstruct(viaDirect, idB);
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
}
@@ -96,8 +96,8 @@ TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstructio
viaCommand.tick();
viaDirect.tick();
}
SimulationTestAccess::demolish(viaCommand, idA);
SimulationTestAccess::demolish(viaDirect, idB);
SimulationTestAccess::deconstruct(viaCommand, idA);
SimulationTestAccess::deconstruct(viaDirect, idB);
CancelDeconstructionCommand command;
command.id = idA;

View File

@@ -22,7 +22,7 @@ GameConfig loadConfig()
constexpr int kScriptTicks = 2000;
// Runs a fixed scripted session and returns the full-state checksum after every
// tick. The script places a small factory, demolishes part of it mid-run, and
// tick. The script places a small factory, deconstructs part of it mid-run, and
// otherwise lets waves/combat run so the RNG stream and ECS state are exercised.
std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
{
@@ -40,7 +40,7 @@ std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
{
if (t == 500)
{
// Demolish the second belt mid-run to exercise the removal paths.
// Deconstruct the second belt mid-run to exercise the removal paths.
SimulationTestAccess::place(sim, BuildingType::Smelter, QPoint(-3, 3), Rotation::East);
}

View File

@@ -77,12 +77,12 @@ TEST_CASE("parseCommand round-trips every command verb", "[replay]")
ClearBeltTilesCommand clear;
clear.tiles = { QPoint(0, 0), QPoint(-1, 4) };
DemolishCommand demolish;
demolish.id = 5;
DeconstructCommand deconstruct;
deconstruct.id = 5;
for (const Command* command : { static_cast<const Command*>(&filters),
static_cast<const Command*>(&clear),
static_cast<const Command*>(&demolish) })
static_cast<const Command*>(&deconstruct) })
{
const std::string text = serializeCommand(*command);
const std::shared_ptr<Command> parsed = parseCommand(text);

View File

@@ -83,11 +83,11 @@ TEST_CASE("serializeCommand: splitter filters are length-prefixed", "[replay]")
== "splitterfilters 4 7 2 iron_ore copper_ore 1 coal");
}
TEST_CASE("serializeCommand: demolish / rotate / schematic / clearbelt", "[replay]")
TEST_CASE("serializeCommand: deconstruct / rotate / schematic / clearbelt", "[replay]")
{
DemolishCommand demolish;
demolish.id = 12;
REQUIRE(serializeCommand(demolish) == "demolish 12");
DeconstructCommand deconstruct;
deconstruct.id = 12;
REQUIRE(serializeCommand(deconstruct) == "deconstruct 12");
RotateInPlaceCommand rotate;
rotate.id = 7;

View File

@@ -33,7 +33,7 @@ struct SimulationTestAccess
return sim.tryPlaceBuilding(type, anchor, rotation);
}
static void demolish(Simulation& sim, BuildingId id) { sim.demolish(id); }
static void deconstruct(Simulation& sim, BuildingId id) { sim.deconstruct(id); }
static void cancelDeconstruction(Simulation& sim, BuildingId id)
{

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)

View File

@@ -2,6 +2,7 @@
#include <map>
#include <optional>
#include <string>
#include <vector>
#include <QWidget>
@@ -10,7 +11,7 @@
#include "BuildHotkeyPressedEvent.h"
#include "BuildingBlocksChangedEvent.h"
#include "BuildingType.h"
#include "DemolishModeChangedEvent.h"
#include "DeconstructModeChangedEvent.h"
#include "EventHandler.h"
#include "GameConfig.h"
#include "UnlockedBuildingsChangedEvent.h"
@@ -20,7 +21,7 @@ class Simulation;
class BuildButtonGrid : public QWidget,
public CombinedEventHandler<BuilderModeExitedEvent,
DemolishModeChangedEvent,
DeconstructModeChangedEvent,
BuildHotkeyPressedEvent,
BuildingBlocksChangedEvent,
UnlockedBuildingsChangedEvent>
@@ -28,7 +29,10 @@ class BuildButtonGrid : public QWidget,
Q_OBJECT
public:
BuildButtonGrid(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr);
// iconDir is the directory holding the per-building "<id>.svg" chip icons
// (REQ-UI-BUILD-GRID); read from disk at runtime, like the config files.
BuildButtonGrid(Simulation* sim, const GameConfig* config,
const std::string& iconDir, QWidget* parent = nullptr);
~BuildButtonGrid() override;
void clearActiveButton();
@@ -44,7 +48,7 @@ private:
void updateVisibility();
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
void handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event) override;
void handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event) override;
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
void handleEvent(std::shared_ptr<const UnlockedBuildingsChangedEvent> event) override;
@@ -55,9 +59,10 @@ private slots:
private:
Simulation* m_sim;
const GameConfig* m_config;
std::string m_iconDir;
std::vector<BuildingType> m_types;
std::vector<QPushButton*> m_buttons;
std::map<BuildingType, int> m_costs;
std::optional<std::size_t> m_activeIndex;
QPushButton* m_demolishButton;
QPushButton* m_deconstructButton;
};

View File

@@ -35,7 +35,7 @@
#include "ReplayPlayer.h"
#include "ReplayReader.h"
#include "ReplayRecorder.h"
#include "DemolishModeChangedEvent.h"
#include "DeconstructModeChangedEvent.h"
#include "EntityHitTest.h"
#include "EntitySelectionChangedEvent.h"
#include "EventManager.h"
@@ -157,7 +157,7 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
, m_ghostRotation(Rotation::East)
, m_ghostValid(false)
, m_dragging(false)
, m_demolishMode(false)
, m_deconstructMode(false)
, m_debugDraw(false)
, m_rng(std::random_device{}())
, m_boxSelecting(false)
@@ -463,7 +463,7 @@ void GameWorldView::paintGL()
drawOverlays(painter);
drawScreenSpace(painter);
drawPauseBorder(painter);
drawDemolishBorder(painter);
drawDeconstructBorder(painter);
drawReplayOverlay(painter);
}
@@ -1984,13 +1984,13 @@ void GameWorldView::drawOverlays(QPainter& painter)
if (!b.queuedForDeconstruction) { continue; }
for (const QPoint& cell : b.bodyCells)
{
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
painter.fillRect(tileRect(cell), m_visuals->overlays.deconstructTint);
}
}
// Demolish tint: while dragging a demolish box, tint every covered
// building/site (REQ-BLD-DEMOLISH-BOX); otherwise tint the hovered one.
if (m_demolishMode && m_boxSelecting)
// Deconstruct tint: while dragging a deconstruct box, tint every covered
// building/site (REQ-BLD-DECONSTRUCT-BOX); otherwise tint the hovered one.
if (m_deconstructMode && m_boxSelecting)
{
for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile))
{
@@ -2004,19 +2004,19 @@ void GameWorldView::drawOverlays(QPainter& painter)
{
for (const QPoint& cell : *cells)
{
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
painter.fillRect(tileRect(cell), m_visuals->overlays.deconstructTint);
}
}
}
}
else if (m_demolishMode && m_demolishHoverBuildingId.has_value())
else if (m_deconstructMode && m_deconstructHoverBuildingId.has_value())
{
const Building* b = m_sim->getBuildings().findBuilding(*m_demolishHoverBuildingId);
const Building* b = m_sim->getBuildings().findBuilding(*m_deconstructHoverBuildingId);
if (b)
{
for (const QPoint& cell : b->bodyCells)
{
painter.fillRect(tileRect(cell), m_visuals->overlays.demolishTint);
painter.fillRect(tileRect(cell), m_visuals->overlays.deconstructTint);
}
}
}
@@ -2124,11 +2124,11 @@ void GameWorldView::drawPauseBorder(QPainter& painter)
drawVignetteBorder(painter, QColor(0, 0, 0, 128)); // 50% black at the edge
}
void GameWorldView::drawDemolishBorder(QPainter& painter)
void GameWorldView::drawDeconstructBorder(QPainter& painter)
{
if (!m_demolishMode) { return; }
// Reuse the demolish overlay color (with its configured alpha) as the edge color.
drawVignetteBorder(painter, m_visuals->overlays.demolishTint);
if (!m_deconstructMode) { return; }
// Reuse the deconstruct overlay color (with its configured alpha) as the edge color.
drawVignetteBorder(painter, m_visuals->overlays.deconstructTint);
}
void GameWorldView::drawVignetteBorder(QPainter& painter, const QColor& edgeColor)
@@ -2325,7 +2325,7 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
case Qt::Key_Q:
if (m_builderType.has_value()) { exitBuilderMode(); }
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
else { toggleDemolishMode(); }
else { toggleDeconstructMode(); }
break;
case Qt::Key_T:
// Request a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP).
@@ -2387,7 +2387,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
}
}
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
else if (m_demolishMode) { toggleDemolishMode(); }
else if (m_deconstructMode) { toggleDeconstructMode(); }
else if (event->modifiers() & Qt::ShiftModifier)
{
// Shift + right-click copies a building's settings, but only in the
@@ -2424,10 +2424,10 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
{
placeBlueprintAtTile(tile);
}
else if (m_demolishMode)
else if (m_deconstructMode)
{
// Start a demolish box drag; a plain click resolves as a 1x1 box on
// release (REQ-BLD-DEMOLISH-CLICK, REQ-BLD-DEMOLISH-BOX).
// Start a deconstruct box drag; a plain click resolves as a 1x1 box on
// release (REQ-BLD-DECONSTRUCT-CLICK, REQ-BLD-DECONSTRUCT-BOX).
m_boxSelecting = true;
m_boxStartTile = tile;
m_boxCurrentTile = tile;
@@ -2601,9 +2601,9 @@ void GameWorldView::mouseMoveEvent(QMouseEvent* event)
{
m_blueprintGhostTile = tile;
}
else if (m_demolishMode)
else if (m_deconstructMode)
{
m_demolishHoverBuildingId = buildingAtTile(tile);
m_deconstructHoverBuildingId = buildingAtTile(tile);
if (m_boxSelecting) { m_boxCurrentTile = tile; }
}
else if (m_boxSelecting)
@@ -2632,12 +2632,12 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
const std::vector<BuildingId> boxIds =
buildingsInBox(m_boxStartTile, m_boxCurrentTile);
if (m_demolishMode)
if (m_deconstructMode)
{
const BuildingSystem& buildings = m_sim->getBuildings();
// Split covered ids into construction sites (removed instantly) and
// operational demolishable buildings (the HQ is protected; player
// operational deconstructible buildings (the HQ is protected; player
// defence stations are not Buildings and never appear in the box).
std::vector<BuildingId> sites;
std::vector<BuildingId> operational;
@@ -2654,11 +2654,11 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
}
}
// Sites: instant demolition with the full refund (REQ-BLD-DEMOLISH).
// Sites: instant demolition with the full refund (REQ-BLD-DECONSTRUCT).
for (BuildingId id : sites)
{
std::shared_ptr<DemolishCommand> command =
std::make_shared<DemolishCommand>();
std::shared_ptr<DeconstructCommand> command =
std::make_shared<DeconstructCommand>();
command->id = id;
enqueueCommand(command);
}
@@ -2666,7 +2666,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
// Operational buildings: if every covered one is already queued,
// un-queue them all; otherwise queue each one not yet queued. A plain
// click is the one-element case, giving the queue/un-queue toggle
// (REQ-BLD-DEMOLISH-BOX, REQ-BLD-DEMOLISH-CLICK).
// (REQ-BLD-DECONSTRUCT-BOX, REQ-BLD-DECONSTRUCT-CLICK).
bool allQueued = !operational.empty();
for (BuildingId id : operational)
{
@@ -2683,14 +2683,14 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
}
else if (!buildings.isQueuedForDeconstruction(id))
{
std::shared_ptr<DemolishCommand> command =
std::make_shared<DemolishCommand>();
std::shared_ptr<DeconstructCommand> command =
std::make_shared<DeconstructCommand>();
command->id = id;
enqueueCommand(command);
}
}
m_demolishHoverBuildingId = std::nullopt;
m_deconstructHoverBuildingId = std::nullopt;
return;
}
@@ -2786,21 +2786,21 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
// Methods (formerly slots)
// ---------------------------------------------------------------------------
void GameWorldView::toggleDemolishMode()
void GameWorldView::toggleDeconstructMode()
{
if (m_demolishMode)
if (m_deconstructMode)
{
m_demolishMode = false;
m_demolishHoverBuildingId = std::nullopt;
m_deconstructMode = false;
m_deconstructHoverBuildingId = std::nullopt;
}
else
{
if (m_builderType.has_value()) { exitBuilderMode(); }
if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
m_demolishMode = true;
m_deconstructMode = true;
}
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(m_demolishMode));
std::make_shared<DeconstructModeChangedEvent>(m_deconstructMode));
}
void GameWorldView::rotateGhost(bool clockwise)
@@ -2931,18 +2931,18 @@ void GameWorldView::enterBuilderMode(BuildingType type)
m_ghostValid = false;
m_tunnelGhostType = BuildingType::TunnelEntry;
m_tunnelPartnerTile.reset();
m_demolishMode = false;
m_deconstructMode = false;
m_blueprintMode.reset();
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(false));
std::make_shared<DeconstructModeChangedEvent>(false));
}
void GameWorldView::enterBlueprintMode(Blueprint blueprint)
{
if (m_builderType.has_value()) { exitBuilderMode(); }
m_demolishMode = false;
m_deconstructMode = false;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(false));
std::make_shared<DeconstructModeChangedEvent>(false));
m_blueprintGhostTile = m_ghostTile;
m_blueprintMode = std::move(blueprint);
}
@@ -2993,10 +2993,10 @@ void GameWorldView::resetForNewGame()
m_schematicChoiceShown = false;
m_ghostRotation = Rotation::East;
m_ghostValid = false;
m_demolishMode = false;
m_demolishHoverBuildingId = std::nullopt;
m_deconstructMode = false;
m_deconstructHoverBuildingId = std::nullopt;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<DemolishModeChangedEvent>(false));
std::make_shared<DeconstructModeChangedEvent>(false));
m_selectedBuildingIds.clear();
clearEntitySelection();
clearScrapSelection();
@@ -3072,9 +3072,9 @@ void GameWorldView::handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEv
exitBuilderMode();
}
void GameWorldView::handleEvent(std::shared_ptr<const DemolishModeToggleRequestedEvent> /*event*/)
void GameWorldView::handleEvent(std::shared_ptr<const DeconstructModeToggleRequestedEvent> /*event*/)
{
toggleDemolishMode();
toggleDeconstructMode();
}
void GameWorldView::handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event)

View File

@@ -23,8 +23,8 @@
#include "BuildingType.h"
#include "BuildingTypeSelectedEvent.h"
#include "BuildingId.h"
#include "DemolishModeChangedEvent.h"
#include "DemolishModeToggleRequestedEvent.h"
#include "DeconstructModeChangedEvent.h"
#include "DeconstructModeToggleRequestedEvent.h"
#include "EventHandler.h"
#include "ExitBlueprintModeRequestedEvent.h"
#include "ExitBuilderModeRequestedEvent.h"
@@ -66,7 +66,7 @@ class GameWorldView : public QOpenGLWidget,
public CombinedEventHandler<BeamFiredEvent,
BuildingTypeSelectedEvent,
ExitBuilderModeRequestedEvent,
DemolishModeToggleRequestedEvent,
DeconstructModeToggleRequestedEvent,
BlueprintPlacementRequestedEvent,
ExitBlueprintModeRequestedEvent,
SpeedChangeRequestedEvent,
@@ -102,7 +102,7 @@ private:
void handleEvent(std::shared_ptr<const BeamFiredEvent> event) override;
void handleEvent(std::shared_ptr<const BuildingTypeSelectedEvent> event) override;
void handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const DemolishModeToggleRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const DeconstructModeToggleRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const ExitBlueprintModeRequestedEvent> event) override;
void handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent> event) override;
@@ -139,11 +139,11 @@ private:
// to make the paused state hard to miss: a black frame whose alpha fades from 50% at
// the viewport edges to 0% toward the center.
void drawPauseBorder(QPainter& painter);
// Vignette-style border shown while demolish mode is active (REQ-UI-DEMOLISH-BORDER),
// tinted with the demolish overlay color (including its configured alpha) from
// Vignette-style border shown while deconstruct mode is active (REQ-UI-DECONSTRUCT-BORDER),
// tinted with the deconstruct overlay color (including its configured alpha) from
// visuals.toml instead of black.
void drawDemolishBorder(QPainter& painter);
// Shared drawing for the pause/demolish vignettes: a mitred picture-frame border
void drawDeconstructBorder(QPainter& painter);
// Shared drawing for the pause/deconstruct vignettes: a mitred picture-frame border
// whose alpha fades from `edgeColor` (with its own alpha) at the viewport edge to
// fully transparent toward the center.
void drawVignetteBorder(QPainter& painter, const QColor& edgeColor);
@@ -252,7 +252,7 @@ private:
void exitBuilderMode();
void enterBlueprintMode(Blueprint blueprint);
void exitBlueprintMode();
void toggleDemolishMode();
void toggleDeconstructMode();
void rotateGhost(bool clockwise);
struct ActiveBeam
@@ -328,8 +328,8 @@ private:
std::vector<CopyConfigFlash> m_copyConfigFlashes;
static constexpr qint64 kCopyFlashDurationMs = 300;
bool m_demolishMode;
std::optional<BuildingId> m_demolishHoverBuildingId;
bool m_deconstructMode;
std::optional<BuildingId> m_deconstructHoverBuildingId;
bool m_debugDraw;
std::vector<BuildingId> m_selectedBuildingIds;

View File

@@ -6,6 +6,7 @@
#include <QApplication>
#include <QCloseEvent>
#include <QDir>
#include <QFile>
#include <QMessageBox>
#include <QPushButton>
@@ -51,8 +52,13 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
sideLayout->setContentsMargins(1, 1, 1, 1);
sideLayout->setSpacing(1);
// 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();
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), m_sidePanel);
m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), iconDir, m_sidePanel);
m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
sideLayout->addWidget(m_selectedBuildingPanel, 1);

View File

@@ -44,7 +44,7 @@ struct OverlayVisuals
{
QColor ghostValid;
QColor ghostInvalid;
QColor demolishTint;
QColor deconstructTint;
QColor selectionRect;
QColor tileHighlight;
QColor selectedOutline;

View File

@@ -220,7 +220,7 @@ VisualsConfig VisualsLoader::load(const std::string& path)
toml::table& ov = requireSubtable(tbl, "overlays", "root");
cfg.overlays.ghostValid = parseColor(requireString(ov, "ghost_valid", "overlays"), "overlays.ghost_valid");
cfg.overlays.ghostInvalid = parseColor(requireString(ov, "ghost_invalid", "overlays"), "overlays.ghost_invalid");
cfg.overlays.demolishTint = parseColor(requireString(ov, "demolish_tint", "overlays"), "overlays.demolish_tint");
cfg.overlays.deconstructTint = parseColor(requireString(ov, "deconstruct_tint", "overlays"), "overlays.deconstruct_tint");
cfg.overlays.selectionRect = parseColor(requireString(ov, "selection_rect", "overlays"), "overlays.selection_rect");
cfg.overlays.tileHighlight = parseColor(requireString(ov, "tile_highlight", "overlays"), "overlays.tile_highlight");
cfg.overlays.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline");