From e8786c39221686f1cbe5aaf76c9e31581aca29bb Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Sat, 4 Jul 2026 15:28:19 +0200 Subject: [PATCH] implement cost formula for asteroid expansion --- bin/app/data/config/world.toml | 4 +-- bin/test/data/config/world.toml | 2 +- docs/requirements.md | 5 +-- src/lib/config/ConfigLoader.cpp | 4 +-- src/lib/config/WorldConfig.h | 4 +-- src/lib/eventsystem/event/CMakeLists.txt | 1 + .../event/ExpansionCostChangedEvent.h | 17 ++++++++++ src/lib/sim/BuildingSystem.cpp | 3 +- src/lib/sim/BuildingSystem.h | 6 ++++ src/lib/sim/Command.h | 9 ++++++ src/lib/sim/Simulation.cpp | 31 +++++++++++++++++++ src/lib/sim/Simulation.h | 12 +++++++ src/test/ConfigLoaderTest.cpp | 8 +++-- src/ui/GameWorldView.cpp | 11 ++++++- src/ui/GameWorldView.h | 1 + src/ui/HeaderBar.cpp | 27 ++++++++++++++++ src/ui/HeaderBar.h | 11 +++++++ 17 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 src/lib/eventsystem/event/ExpansionCostChangedEvent.h diff --git a/bin/app/data/config/world.toml b/bin/app/data/config/world.toml index 9424e09..539cce0 100644 --- a/bin/app/data/config/world.toml +++ b/bin/app/data/config/world.toml @@ -19,9 +19,7 @@ enemy_buffer_width_tiles = 20 [expansion] columns_per_expansion_tiles = 10 -# Flat until the escalating cost formula lands (progression_design.md -# action item 4). -cost_building_blocks = 400 +cost_building_blocks_formula = "300 + 50*x + 10*x*x" [push] push_expand_columns_tiles = 10 diff --git a/bin/test/data/config/world.toml b/bin/test/data/config/world.toml index acb7d1f..1bf2e91 100644 --- a/bin/test/data/config/world.toml +++ b/bin/test/data/config/world.toml @@ -19,7 +19,7 @@ enemy_buffer_width_tiles = 15 [expansion] columns_per_expansion_tiles = 10 -cost_building_blocks = 200 +cost_building_blocks_formula = "400 * 2^x" [push] push_expand_columns_tiles = 20 diff --git a/docs/requirements.md b/docs/requirements.md index f88a69b..9c524b3 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -367,7 +367,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des ## Asteroid Expansion - REQ-EXP-UNLOCK: The player can unlock additional asteroid tile columns to the left of the existing asteroid by spending building blocks from the global stock. -- REQ-EXP-COST: Each expansion adds `world.toml [expansion].columns_per_expansion` columns and costs `[expansion].cost_building_blocks` building blocks. +- REQ-EXP-COST: Each expansion adds `world.toml [expansion].columns_per_expansion` columns. The building block cost of an expansion is defined by the formula `world.toml [expansion].cost_building_blocks_formula`, where `x` is the number of expansions already purchased (0 for the first expansion, incrementing by 1 for each subsequent expansion). The formula is evaluated at purchase time and its result is floored to an integer number of building blocks. ## UI @@ -392,9 +392,10 @@ The screen is divided into two columns: a main column (75% width) containing the (75% width) (25% width) ``` -- REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time, the current global building blocks stock, and the artifact count (REQ-WIN-ARTIFACT-COUNT) displayed as `Artifacts: x/y` (where `x` is the current artifact count and `y` is `world.toml [world].artifact_win_count`) on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) to the left of the speed buttons, and game speed controls on the right. +- REQ-UI-HEADER: The header bar spans the width of the game world column (75% of the screen width) and always shows the elapsed survival time, the current global building blocks stock, and the artifact count (REQ-WIN-ARTIFACT-COUNT) displayed as `Artifacts: x/y` (where `x` is the current artifact count and `y` is `world.toml [world].artifact_win_count`) on the left, the boss wave counter and boss countdown (REQ-UI-BOSS-STATUS) and an asteroid expansion button (REQ-UI-EXPAND-BUTTON) to the left of the speed buttons, and game speed controls on the right. - REQ-UI-BOSS-STATUS: The header bar displays, to the left of the speed buttons, the current boss wave counter (REQ-WAV-BOSS-COUNTER) and the time remaining on the boss countdown (REQ-WAV-BOSS-COUNTDOWN). The boss wave counter is shown as `Boss Wave #` and the countdown as `Next boss: `, where `` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs. - REQ-UI-SPEED: The game speed controls in the header bar are buttons for 0×, 0.5×, 1×, 2×, and 4× speed. The currently active speed is shown as selected. All game simulation (production, movement, threat accumulation, wave timing) scales with the selected speed. 0× pauses the game. +- REQ-UI-EXPAND-BUTTON: The header bar shows an asteroid expansion button captioned `Expand: Blocks`, where `` is the current expansion cost computed from `world.toml [expansion].cost_building_blocks_formula` at the current number of purchased expansions (REQ-EXP-COST). Clicking the button unlocks the next asteroid expansion (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND), spending that many building blocks from the global stock. The button is disabled when the player cannot currently afford the cost (consistent with REQ-UI-BUILD-DISABLED). The caption updates as the cost changes with each purchased expansion. - REQ-UI-WORLD-SIZE: The game world view occupies the full height below the header bar in the main column (75% of the screen width). - REQ-UI-PANEL-COLUMN: The side panel column occupies 25% of the screen width and the full screen height. It is divided into three equal-height panels stacked top to bottom: selected building panel (top), build button grid (middle), and blueprint panel (bottom). diff --git a/src/lib/config/ConfigLoader.cpp b/src/lib/config/ConfigLoader.cpp index 88f3da7..a5adfff 100644 --- a/src/lib/config/ConfigLoader.cpp +++ b/src/lib/config/ConfigLoader.cpp @@ -278,8 +278,8 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path) cfg.regions.contestZoneWidth_tiles = static_cast(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles")); cfg.regions.enemyBufferWidth_tiles = static_cast(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles")); - cfg.expansion.columnsPerExpansion_tiles = static_cast(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles")); - cfg.expansion.costBuildingBlocks = static_cast(requireInt(tbl["expansion"]["cost_building_blocks"], file, "expansion.cost_building_blocks")); + cfg.expansion.columnsPerExpansion_tiles = static_cast(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles")); + cfg.expansion.costBuildingBlocksFormula = requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula"); cfg.push.pushExpandColumns_tiles = static_cast(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles")); cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds"); diff --git a/src/lib/config/WorldConfig.h b/src/lib/config/WorldConfig.h index 4d7a5c9..8e9f755 100644 --- a/src/lib/config/WorldConfig.h +++ b/src/lib/config/WorldConfig.h @@ -14,8 +14,8 @@ struct WorldRegions // Asteroid expansion (REQ-EXP-UNLOCK, REQ-EXP-COST). struct WorldExpansion { - int columnsPerExpansion_tiles; - int costBuildingBlocks; + int columnsPerExpansion_tiles; + Formula costBuildingBlocksFormula; // cost in building blocks; x = expansions already purchased }; // Push effects (REQ-PSH-*, REQ-WAV-BOSS-ADVANCE). diff --git a/src/lib/eventsystem/event/CMakeLists.txt b/src/lib/eventsystem/event/CMakeLists.txt index 1125058..5cef778 100644 --- a/src/lib/eventsystem/event/CMakeLists.txt +++ b/src/lib/eventsystem/event/CMakeLists.txt @@ -3,6 +3,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h + ${CMAKE_CURRENT_SOURCE_DIR}/ExpansionCostChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h diff --git a/src/lib/eventsystem/event/ExpansionCostChangedEvent.h b/src/lib/eventsystem/event/ExpansionCostChangedEvent.h new file mode 100644 index 0000000..dcd7b1e --- /dev/null +++ b/src/lib/eventsystem/event/ExpansionCostChangedEvent.h @@ -0,0 +1,17 @@ +#ifndef EXPANSION_COST_CHANGED_EVENT_H +#define EXPANSION_COST_CHANGED_EVENT_H + +#include "Event.h" + +// Fired when the current asteroid-expansion cost changes (REQ-EXP-COST): once at +// startup and again after each expansion is purchased. Carries the cost in +// building blocks so the header Expand button can update its caption/enabled +// state (REQ-UI-EXPAND-BUTTON). +class ExpansionCostChangedEvent : public Event +{ +public: + explicit ExpansionCostChangedEvent(int cost) : cost(cost) {} + const int cost; +}; + +#endif // EXPANSION_COST_CHANGED_EVENT_H diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 8343c2e..85df1d2 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -24,6 +24,7 @@ BuildingSystem::BuildingSystem(const GameConfig& config, , m_spawnShip(std::move(spawnShip)) , m_isItemUnlocked(std::move(isItemUnlocked)) , m_rng(rng) + , m_asteroidWidth_tiles(config.world.regions.asteroidWidth_tiles) { } @@ -281,7 +282,7 @@ bool BuildingSystem::bodyCellsWithinWorldBounds(const std::vector& bodyC QPoint anchor) const { const int heightTiles = m_config.world.heightTiles; - const int leftEdgeX = -m_config.world.regions.asteroidWidth_tiles; + const int leftEdgeX = -m_asteroidWidth_tiles; for (const QPoint& cell : bodyCells) { const QPoint worldCell = anchor + cell; diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index b631c06..cff3eea 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -59,6 +59,11 @@ public: bool isPlacementValid(BuildingType type, QPoint anchor, Rotation rotation) const; + // Sets the current buildable asteroid width in tiles. Grows the left + // placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK). + // Defaults to world.regions.asteroid_width_tiles at construction. + void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; } + // Remove a building or construction site by id. Returns the refund in // building blocks (floor(cost * refundPercentage / 100)). Returns 0 for // unknown ids. @@ -179,6 +184,7 @@ private: const std::optional&)> m_spawnShip; std::function m_isItemUnlocked; std::mt19937& m_rng; + int m_asteroidWidth_tiles; std::vector m_buildings; std::deque m_constructionQueue; diff --git a/src/lib/sim/Command.h b/src/lib/sim/Command.h index 20240e4..5551bb5 100644 --- a/src/lib/sim/Command.h +++ b/src/lib/sim/Command.h @@ -35,6 +35,7 @@ enum class CommandKind SetSplitterFilters, ClearBeltTiles, ApplySchematicChoice, + ExpandAsteroid, Reset }; @@ -129,6 +130,14 @@ struct ApplySchematicChoiceCommand : Command int choiceIndex = 0; }; +// Unlocks the next asteroid expansion (REQ-EXP-UNLOCK). Carries no payload: the +// simulation derives the cost and column count from its own expansion counter +// and config, so a recorded command replays identically. +struct ExpandAsteroidCommand : Command +{ + ExpandAsteroidCommand() : Command(CommandKind::ExpandAsteroid) {} +}; + // Restart boundary: reinitializes the simulation with a fresh seed and, if // config is set, a reloaded config (GameConfig is move-only, so it is carried by // shared_ptr and moved into the sim on apply). A null config keeps the current diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index df2dc2c..ab0c683 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "AiSystem.h" #include "Command.h" @@ -132,6 +133,7 @@ void Simulation::reset(unsigned int seed) m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds); m_nextBuildingId = 1; m_buildingBlocksStock = m_config.world.startingBuildingBlocks; + m_expansionsPurchased = 0; m_gameOver = false; m_isWon = false; m_artifactCount = 0; @@ -275,6 +277,9 @@ void Simulation::apply(const Command& command) case CommandKind::ApplySchematicChoice: applySchematicChoice(static_cast(command).choiceIndex); break; + case CommandKind::ExpandAsteroid: + tryExpandAsteroid(); + break; case CommandKind::Reset: { const ResetCommand& c = static_cast(command); @@ -993,6 +998,7 @@ unsigned long long Simulation::computeStateChecksum() const hasher.append(m_gameOver); hasher.append(m_isWon); hasher.append(m_artifactCount); + hasher.append(m_expansionsPurchased); // WaveSystem scalar state, reached through existing accessors. hasher.append(threatLevel()); @@ -1100,6 +1106,31 @@ int Simulation::buildingBlocksStock() const return m_buildingBlocksStock; } +int Simulation::currentAsteroidWidth_tiles() const +{ + return m_config.world.regions.asteroidWidth_tiles + + m_expansionsPurchased * m_config.world.expansion.columnsPerExpansion_tiles; +} + +int Simulation::currentExpansionCost() const +{ + const double cost = m_config.world.expansion.costBuildingBlocksFormula.evaluate( + static_cast(m_expansionsPurchased)); + return static_cast(std::floor(cost)); +} + +void Simulation::tryExpandAsteroid() +{ + const int cost = currentExpansionCost(); + if (m_buildingBlocksStock < cost) + { + return; + } + m_buildingBlocksStock -= cost; + ++m_expansionsPurchased; + m_buildingSystem->setAsteroidWidth_tiles(currentAsteroidWidth_tiles()); +} + bool Simulation::isGameOver() const { return m_gameOver; diff --git a/src/lib/sim/Simulation.h b/src/lib/sim/Simulation.h index bff8b8b..b79ed1d 100644 --- a/src/lib/sim/Simulation.h +++ b/src/lib/sim/Simulation.h @@ -72,6 +72,12 @@ public: // The seed this run was (re)initialized with; written to the replay header. unsigned int getSeed() const; int buildingBlocksStock() const; + // Current asteroid width in tiles = base width + purchased expansions + // (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND). + int currentAsteroidWidth_tiles() const; + // Building block cost of the next expansion, floored to an integer + // (REQ-EXP-COST); x = number of expansions already purchased. + int currentExpansionCost() const; bool isGameOver() const; bool isWon() const; int artifactCount() const; @@ -136,6 +142,11 @@ private: // Clears the pending choices after application. void applySchematicChoice(int choiceIndex); + // Unlocks one asteroid expansion if affordable (REQ-EXP-UNLOCK): checks the + // current cost against the stock, deducts it, increments the expansion + // counter, and widens the buildable asteroid. No-op if blocks are short. + void tryExpandAsteroid(); + // Mutable subsystem accessors; same chokepoint rule as the mutators above. BuildingSystem& buildingsMutable(); BeltSystem& beltsMutable(); @@ -165,6 +176,7 @@ private: Tick m_nextDepartureTick; BuildingId m_nextBuildingId; int m_buildingBlocksStock; + int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x bool m_gameOver = false; bool m_isWon = false; int m_artifactCount = 0; diff --git a/src/test/ConfigLoaderTest.cpp b/src/test/ConfigLoaderTest.cpp index 28e7a48..99c93bd 100644 --- a/src/test/ConfigLoaderTest.cpp +++ b/src/test/ConfigLoaderTest.cpp @@ -76,6 +76,8 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c REQUIRE(cfg.world.regions.playerBufferWidth_tiles == 10); REQUIRE(cfg.world.regions.enemyBufferWidth_tiles == 15); REQUIRE(cfg.world.expansion.columnsPerExpansion_tiles == 10); + REQUIRE(cfg.world.expansion.costBuildingBlocksFormula.evaluate(0) == Approx(400.0)); + REQUIRE(cfg.world.expansion.costBuildingBlocksFormula.evaluate(1) == Approx(800.0)); REQUIRE(cfg.world.push.bossAdvanceSeconds == Approx(60.0)); REQUIRE(cfg.world.orbitFactor == Approx(0.8)); REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0)); @@ -186,7 +188,7 @@ contest_zone_width_tiles = 30 [expansion] columns_per_expansion_tiles = 10 -cost_building_blocks = 200 +cost_building_blocks_formula = "400 * 2^x" [push] push_expand_columns_tiles = 20 @@ -237,7 +239,7 @@ enemy_buffer_width_tiles = 15 [expansion] columns_per_expansion_tiles = 10 -cost_building_blocks = 200 +cost_building_blocks_formula = "400 * 2^x" [push] push_expand_columns_tiles = 20 @@ -284,7 +286,7 @@ enemy_buffer_width_tiles = 15 [expansion] columns_per_expansion_tiles = 10 -cost_building_blocks = 200 +cost_building_blocks_formula = "400 * 2^x" [push] push_expand_columns_tiles = 20 diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 0849a03..994de52 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -59,6 +59,7 @@ #include "BuilderModeExitedEvent.h" #include "BlueprintModeExitedEvent.h" #include "BuildingBlocksChangedEvent.h" +#include "ExpansionCostChangedEvent.h" #include "GameSpeedChangedEvent.h" #include "SchematicChoicesAvailableEvent.h" #include "TickAdvancedEvent.h" @@ -276,6 +277,7 @@ void GameWorldView::onFrame() { const Tick newTick = m_sim->currentTick(); const int newBlocks = m_sim->buildingBlocksStock(); + const int newExpCost = m_sim->currentExpansionCost(); const int newBoss = m_sim->bossWaveCounter(); const Tick newCountdown = m_sim->bossCountdownTicks(); @@ -291,6 +293,12 @@ void GameWorldView::onFrame() EventManager::getInstance()->sendEventImmediately( std::make_shared(newBlocks)); } + if (newExpCost != m_lastExpansionCost) + { + m_lastExpansionCost = newExpCost; + EventManager::getInstance()->sendEventImmediately( + std::make_shared(newExpCost)); + } if (newBoss != m_lastBossCounter || newCountdown != m_lastBossCountdown) { m_lastBossCounter = newBoss; @@ -435,7 +443,7 @@ QRect GameWorldView::viewportRect() const float GameWorldView::asteroidLeftEdge() const { - float leftX = -static_cast(m_config->world.regions.asteroidWidth_tiles); + float leftX = -static_cast(m_sim->currentAsteroidWidth_tiles()); for (const Building& b : m_sim->buildings().allBuildings()) { for (const QPoint& cell : b.bodyCells) @@ -1827,6 +1835,7 @@ void GameWorldView::resetForNewGame() m_prevNonZeroSpeed = 1.0; m_lastTick = Tick(-1); m_lastBlocks = -1; + m_lastExpansionCost = -1; m_lastBossCounter = -1; m_lastBossCountdown = Tick(-1); m_lastArtifactCount = -1; diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index 74a67e5..b8dc0c7 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -226,6 +226,7 @@ private: Tick m_lastTick = Tick(-1); int m_lastBlocks = -1; + int m_lastExpansionCost = -1; int m_lastBossCounter = -1; Tick m_lastBossCountdown = Tick(-1); int m_lastArtifactCount = -1; diff --git a/src/ui/HeaderBar.cpp b/src/ui/HeaderBar.cpp index d87c165..06db4c6 100644 --- a/src/ui/HeaderBar.cpp +++ b/src/ui/HeaderBar.cpp @@ -8,6 +8,8 @@ #include #include +#include "Command.h" +#include "CommandRequestedEvent.h" #include "EventManager.h" #include "SpeedChangeRequestedEvent.h" #include "Tick.h" @@ -32,6 +34,17 @@ HeaderBar::HeaderBar(QWidget* parent) layout->addStretch(); layout->addWidget(m_bossLabel); + // Asteroid expansion button, to the left of the speed buttons (REQ-UI-HEADER, + // REQ-UI-EXPAND-BUTTON). Caption/enabled state are set on the first + // ExpansionCostChangedEvent; clicking requests an ExpandAsteroidCommand. + m_expandButton = new QPushButton(tr("Expand"), this); + layout->addWidget(m_expandButton); + connect(m_expandButton, &QPushButton::clicked, this, []() { + EventManager::getInstance()->sendEventImmediately( + std::make_shared( + std::make_shared())); + }); + const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" }; QSignalMapper* mapper = new QSignalMapper(this); for (int i = 0; i < kSpeedCount; ++i) @@ -67,7 +80,21 @@ void HeaderBar::handleEvent(std::shared_ptr event) void HeaderBar::handleEvent(std::shared_ptr event) { + m_blocks = event->blocks; m_blocksLabel->setText(tr("Blocks: %1").arg(event->blocks)); + updateExpandButton(); +} + +void HeaderBar::handleEvent(std::shared_ptr event) +{ + m_expansionCost = event->cost; + updateExpandButton(); +} + +void HeaderBar::updateExpandButton() +{ + m_expandButton->setText(tr("Expand: %1 Blocks").arg(m_expansionCost)); + m_expandButton->setEnabled(m_blocks >= m_expansionCost); } void HeaderBar::handleEvent(std::shared_ptr event) diff --git a/src/ui/HeaderBar.h b/src/ui/HeaderBar.h index 4cbba9d..b2cd47a 100644 --- a/src/ui/HeaderBar.h +++ b/src/ui/HeaderBar.h @@ -8,6 +8,7 @@ #include "BossWaveUpdatedEvent.h" #include "BuildingBlocksChangedEvent.h" #include "EventHandler.h" +#include "ExpansionCostChangedEvent.h" #include "GameSpeedChangedEvent.h" #include "Tick.h" #include "TickAdvancedEvent.h" @@ -18,6 +19,7 @@ class QPushButton; class HeaderBar : public QWidget, public CombinedEventHandler @@ -34,16 +36,25 @@ private slots: private: void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; + void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; void handleEvent(std::shared_ptr event) override; + // Refreshes the Expand button caption and enabled state from the current + // expansion cost and building block stock (REQ-UI-EXPAND-BUTTON). + void updateExpandButton(); + QLabel* m_timeLabel; QLabel* m_blocksLabel; QLabel* m_artifactsLabel; QLabel* m_bossLabel; + QPushButton* m_expandButton; std::vector m_speedButtons; + int m_blocks = 0; + int m_expansionCost = 0; + static const double kSpeeds[]; static const int kSpeedCount; };