implement cost formula for asteroid expansion

This commit is contained in:
2026-07-04 15:28:19 +02:00
parent fc622670d2
commit e8786c3922
17 changed files with 141 additions and 15 deletions

View File

@@ -19,9 +19,7 @@ enemy_buffer_width_tiles = 20
[expansion] [expansion]
columns_per_expansion_tiles = 10 columns_per_expansion_tiles = 10
# Flat until the escalating cost formula lands (progression_design.md cost_building_blocks_formula = "300 + 50*x + 10*x*x"
# action item 4).
cost_building_blocks = 400
[push] [push]
push_expand_columns_tiles = 10 push_expand_columns_tiles = 10

View File

@@ -19,7 +19,7 @@ enemy_buffer_width_tiles = 15
[expansion] [expansion]
columns_per_expansion_tiles = 10 columns_per_expansion_tiles = 10
cost_building_blocks = 200 cost_building_blocks_formula = "400 * 2^x"
[push] [push]
push_expand_columns_tiles = 20 push_expand_columns_tiles = 20

View File

@@ -367,7 +367,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
## Asteroid Expansion ## 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-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 ## UI
@@ -392,9 +392,10 @@ The screen is divided into two columns: a main column (75% width) containing the
(75% width) (25% width) (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 #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` is the remaining seconds formatted as whole minutes and two-digit seconds. Both values update continuously as the simulation runs. - 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 #<x>` and the countdown as `Next boss: <M:SS>`, where `<M:SS>` 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-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: <x> Blocks`, where `<x>` 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-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). - 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).

View File

@@ -278,8 +278,8 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles")); cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
cfg.regions.enemyBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles")); cfg.regions.enemyBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles"));
cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles")); cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
cfg.expansion.costBuildingBlocks = static_cast<int>(requireInt(tbl["expansion"]["cost_building_blocks"], file, "expansion.cost_building_blocks")); cfg.expansion.costBuildingBlocksFormula = requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula");
cfg.push.pushExpandColumns_tiles = static_cast<int>(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles")); cfg.push.pushExpandColumns_tiles = static_cast<int>(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"); cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");

View File

@@ -14,8 +14,8 @@ struct WorldRegions
// Asteroid expansion (REQ-EXP-UNLOCK, REQ-EXP-COST). // Asteroid expansion (REQ-EXP-UNLOCK, REQ-EXP-COST).
struct WorldExpansion struct WorldExpansion
{ {
int columnsPerExpansion_tiles; int columnsPerExpansion_tiles;
int costBuildingBlocks; Formula costBuildingBlocksFormula; // cost in building blocks; x = expansions already purchased
}; };
// Push effects (REQ-PSH-*, REQ-WAV-BOSS-ADVANCE). // Push effects (REQ-PSH-*, REQ-WAV-BOSS-ADVANCE).

View File

@@ -3,6 +3,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/TracePrintRequestedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/TickAdvancedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/BuildingBlocksChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/ExpansionCostChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h ${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h

View File

@@ -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

View File

@@ -24,6 +24,7 @@ BuildingSystem::BuildingSystem(const GameConfig& config,
, m_spawnShip(std::move(spawnShip)) , m_spawnShip(std::move(spawnShip))
, m_isItemUnlocked(std::move(isItemUnlocked)) , m_isItemUnlocked(std::move(isItemUnlocked))
, m_rng(rng) , m_rng(rng)
, m_asteroidWidth_tiles(config.world.regions.asteroidWidth_tiles)
{ {
} }
@@ -281,7 +282,7 @@ bool BuildingSystem::bodyCellsWithinWorldBounds(const std::vector<QPoint>& bodyC
QPoint anchor) const QPoint anchor) const
{ {
const int heightTiles = m_config.world.heightTiles; 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) for (const QPoint& cell : bodyCells)
{ {
const QPoint worldCell = anchor + cell; const QPoint worldCell = anchor + cell;

View File

@@ -59,6 +59,11 @@ public:
bool isPlacementValid(BuildingType type, QPoint anchor, bool isPlacementValid(BuildingType type, QPoint anchor,
Rotation rotation) const; 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 // Remove a building or construction site by id. Returns the refund in
// building blocks (floor(cost * refundPercentage / 100)). Returns 0 for // building blocks (floor(cost * refundPercentage / 100)). Returns 0 for
// unknown ids. // unknown ids.
@@ -179,6 +184,7 @@ private:
const std::optional<ShipLayoutConfig>&)> m_spawnShip; const std::optional<ShipLayoutConfig>&)> m_spawnShip;
std::function<bool(const std::string&)> m_isItemUnlocked; std::function<bool(const std::string&)> m_isItemUnlocked;
std::mt19937& m_rng; std::mt19937& m_rng;
int m_asteroidWidth_tiles;
std::vector<Building> m_buildings; std::vector<Building> m_buildings;
std::deque<ConstructionSite> m_constructionQueue; std::deque<ConstructionSite> m_constructionQueue;

View File

@@ -35,6 +35,7 @@ enum class CommandKind
SetSplitterFilters, SetSplitterFilters,
ClearBeltTiles, ClearBeltTiles,
ApplySchematicChoice, ApplySchematicChoice,
ExpandAsteroid,
Reset Reset
}; };
@@ -129,6 +130,14 @@ struct ApplySchematicChoiceCommand : Command
int choiceIndex = 0; 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 // 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 // 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 // shared_ptr and moved into the sim on apply). A null config keeps the current

View File

@@ -2,6 +2,7 @@
#include <algorithm> #include <algorithm>
#include <cassert> #include <cassert>
#include <cmath>
#include "AiSystem.h" #include "AiSystem.h"
#include "Command.h" #include "Command.h"
@@ -132,6 +133,7 @@ void Simulation::reset(unsigned int seed)
m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds); m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds);
m_nextBuildingId = 1; m_nextBuildingId = 1;
m_buildingBlocksStock = m_config.world.startingBuildingBlocks; m_buildingBlocksStock = m_config.world.startingBuildingBlocks;
m_expansionsPurchased = 0;
m_gameOver = false; m_gameOver = false;
m_isWon = false; m_isWon = false;
m_artifactCount = 0; m_artifactCount = 0;
@@ -275,6 +277,9 @@ void Simulation::apply(const Command& command)
case CommandKind::ApplySchematicChoice: case CommandKind::ApplySchematicChoice:
applySchematicChoice(static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex); applySchematicChoice(static_cast<const ApplySchematicChoiceCommand&>(command).choiceIndex);
break; break;
case CommandKind::ExpandAsteroid:
tryExpandAsteroid();
break;
case CommandKind::Reset: case CommandKind::Reset:
{ {
const ResetCommand& c = static_cast<const ResetCommand&>(command); const ResetCommand& c = static_cast<const ResetCommand&>(command);
@@ -993,6 +998,7 @@ unsigned long long Simulation::computeStateChecksum() const
hasher.append(m_gameOver); hasher.append(m_gameOver);
hasher.append(m_isWon); hasher.append(m_isWon);
hasher.append(m_artifactCount); hasher.append(m_artifactCount);
hasher.append(m_expansionsPurchased);
// WaveSystem scalar state, reached through existing accessors. // WaveSystem scalar state, reached through existing accessors.
hasher.append(threatLevel()); hasher.append(threatLevel());
@@ -1100,6 +1106,31 @@ int Simulation::buildingBlocksStock() const
return m_buildingBlocksStock; 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<double>(m_expansionsPurchased));
return static_cast<int>(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 bool Simulation::isGameOver() const
{ {
return m_gameOver; return m_gameOver;

View File

@@ -72,6 +72,12 @@ public:
// The seed this run was (re)initialized with; written to the replay header. // The seed this run was (re)initialized with; written to the replay header.
unsigned int getSeed() const; unsigned int getSeed() const;
int buildingBlocksStock() 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 isGameOver() const;
bool isWon() const; bool isWon() const;
int artifactCount() const; int artifactCount() const;
@@ -136,6 +142,11 @@ private:
// Clears the pending choices after application. // Clears the pending choices after application.
void applySchematicChoice(int choiceIndex); 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. // Mutable subsystem accessors; same chokepoint rule as the mutators above.
BuildingSystem& buildingsMutable(); BuildingSystem& buildingsMutable();
BeltSystem& beltsMutable(); BeltSystem& beltsMutable();
@@ -165,6 +176,7 @@ private:
Tick m_nextDepartureTick; Tick m_nextDepartureTick;
BuildingId m_nextBuildingId; BuildingId m_nextBuildingId;
int m_buildingBlocksStock; int m_buildingBlocksStock;
int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x
bool m_gameOver = false; bool m_gameOver = false;
bool m_isWon = false; bool m_isWon = false;
int m_artifactCount = 0; int m_artifactCount = 0;

View File

@@ -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.playerBufferWidth_tiles == 10);
REQUIRE(cfg.world.regions.enemyBufferWidth_tiles == 15); REQUIRE(cfg.world.regions.enemyBufferWidth_tiles == 15);
REQUIRE(cfg.world.expansion.columnsPerExpansion_tiles == 10); 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.push.bossAdvanceSeconds == Approx(60.0));
REQUIRE(cfg.world.orbitFactor == Approx(0.8)); REQUIRE(cfg.world.orbitFactor == Approx(0.8));
REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0)); REQUIRE(cfg.world.rallyOrbitRadius_tiles == Approx(5.0));
@@ -186,7 +188,7 @@ contest_zone_width_tiles = 30
[expansion] [expansion]
columns_per_expansion_tiles = 10 columns_per_expansion_tiles = 10
cost_building_blocks = 200 cost_building_blocks_formula = "400 * 2^x"
[push] [push]
push_expand_columns_tiles = 20 push_expand_columns_tiles = 20
@@ -237,7 +239,7 @@ enemy_buffer_width_tiles = 15
[expansion] [expansion]
columns_per_expansion_tiles = 10 columns_per_expansion_tiles = 10
cost_building_blocks = 200 cost_building_blocks_formula = "400 * 2^x"
[push] [push]
push_expand_columns_tiles = 20 push_expand_columns_tiles = 20
@@ -284,7 +286,7 @@ enemy_buffer_width_tiles = 15
[expansion] [expansion]
columns_per_expansion_tiles = 10 columns_per_expansion_tiles = 10
cost_building_blocks = 200 cost_building_blocks_formula = "400 * 2^x"
[push] [push]
push_expand_columns_tiles = 20 push_expand_columns_tiles = 20

View File

@@ -59,6 +59,7 @@
#include "BuilderModeExitedEvent.h" #include "BuilderModeExitedEvent.h"
#include "BlueprintModeExitedEvent.h" #include "BlueprintModeExitedEvent.h"
#include "BuildingBlocksChangedEvent.h" #include "BuildingBlocksChangedEvent.h"
#include "ExpansionCostChangedEvent.h"
#include "GameSpeedChangedEvent.h" #include "GameSpeedChangedEvent.h"
#include "SchematicChoicesAvailableEvent.h" #include "SchematicChoicesAvailableEvent.h"
#include "TickAdvancedEvent.h" #include "TickAdvancedEvent.h"
@@ -276,6 +277,7 @@ void GameWorldView::onFrame()
{ {
const Tick newTick = m_sim->currentTick(); const Tick newTick = m_sim->currentTick();
const int newBlocks = m_sim->buildingBlocksStock(); const int newBlocks = m_sim->buildingBlocksStock();
const int newExpCost = m_sim->currentExpansionCost();
const int newBoss = m_sim->bossWaveCounter(); const int newBoss = m_sim->bossWaveCounter();
const Tick newCountdown = m_sim->bossCountdownTicks(); const Tick newCountdown = m_sim->bossCountdownTicks();
@@ -291,6 +293,12 @@ void GameWorldView::onFrame()
EventManager::getInstance()->sendEventImmediately( EventManager::getInstance()->sendEventImmediately(
std::make_shared<BuildingBlocksChangedEvent>(newBlocks)); std::make_shared<BuildingBlocksChangedEvent>(newBlocks));
} }
if (newExpCost != m_lastExpansionCost)
{
m_lastExpansionCost = newExpCost;
EventManager::getInstance()->sendEventImmediately(
std::make_shared<ExpansionCostChangedEvent>(newExpCost));
}
if (newBoss != m_lastBossCounter || newCountdown != m_lastBossCountdown) if (newBoss != m_lastBossCounter || newCountdown != m_lastBossCountdown)
{ {
m_lastBossCounter = newBoss; m_lastBossCounter = newBoss;
@@ -435,7 +443,7 @@ QRect GameWorldView::viewportRect() const
float GameWorldView::asteroidLeftEdge() const float GameWorldView::asteroidLeftEdge() const
{ {
float leftX = -static_cast<float>(m_config->world.regions.asteroidWidth_tiles); float leftX = -static_cast<float>(m_sim->currentAsteroidWidth_tiles());
for (const Building& b : m_sim->buildings().allBuildings()) for (const Building& b : m_sim->buildings().allBuildings())
{ {
for (const QPoint& cell : b.bodyCells) for (const QPoint& cell : b.bodyCells)
@@ -1827,6 +1835,7 @@ void GameWorldView::resetForNewGame()
m_prevNonZeroSpeed = 1.0; m_prevNonZeroSpeed = 1.0;
m_lastTick = Tick(-1); m_lastTick = Tick(-1);
m_lastBlocks = -1; m_lastBlocks = -1;
m_lastExpansionCost = -1;
m_lastBossCounter = -1; m_lastBossCounter = -1;
m_lastBossCountdown = Tick(-1); m_lastBossCountdown = Tick(-1);
m_lastArtifactCount = -1; m_lastArtifactCount = -1;

View File

@@ -226,6 +226,7 @@ private:
Tick m_lastTick = Tick(-1); Tick m_lastTick = Tick(-1);
int m_lastBlocks = -1; int m_lastBlocks = -1;
int m_lastExpansionCost = -1;
int m_lastBossCounter = -1; int m_lastBossCounter = -1;
Tick m_lastBossCountdown = Tick(-1); Tick m_lastBossCountdown = Tick(-1);
int m_lastArtifactCount = -1; int m_lastArtifactCount = -1;

View File

@@ -8,6 +8,8 @@
#include <QPushButton> #include <QPushButton>
#include <QSignalMapper> #include <QSignalMapper>
#include "Command.h"
#include "CommandRequestedEvent.h"
#include "EventManager.h" #include "EventManager.h"
#include "SpeedChangeRequestedEvent.h" #include "SpeedChangeRequestedEvent.h"
#include "Tick.h" #include "Tick.h"
@@ -32,6 +34,17 @@ HeaderBar::HeaderBar(QWidget* parent)
layout->addStretch(); layout->addStretch();
layout->addWidget(m_bossLabel); 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<CommandRequestedEvent>(
std::make_shared<ExpandAsteroidCommand>()));
});
const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" }; const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" };
QSignalMapper* mapper = new QSignalMapper(this); QSignalMapper* mapper = new QSignalMapper(this);
for (int i = 0; i < kSpeedCount; ++i) for (int i = 0; i < kSpeedCount; ++i)
@@ -67,7 +80,21 @@ void HeaderBar::handleEvent(std::shared_ptr<const TickAdvancedEvent> event)
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
{ {
m_blocks = event->blocks;
m_blocksLabel->setText(tr("Blocks: %1").arg(event->blocks)); m_blocksLabel->setText(tr("Blocks: %1").arg(event->blocks));
updateExpandButton();
}
void HeaderBar::handleEvent(std::shared_ptr<const ExpansionCostChangedEvent> 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<const GameSpeedChangedEvent> event) void HeaderBar::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)

View File

@@ -8,6 +8,7 @@
#include "BossWaveUpdatedEvent.h" #include "BossWaveUpdatedEvent.h"
#include "BuildingBlocksChangedEvent.h" #include "BuildingBlocksChangedEvent.h"
#include "EventHandler.h" #include "EventHandler.h"
#include "ExpansionCostChangedEvent.h"
#include "GameSpeedChangedEvent.h" #include "GameSpeedChangedEvent.h"
#include "Tick.h" #include "Tick.h"
#include "TickAdvancedEvent.h" #include "TickAdvancedEvent.h"
@@ -18,6 +19,7 @@ class QPushButton;
class HeaderBar : public QWidget, class HeaderBar : public QWidget,
public CombinedEventHandler<TickAdvancedEvent, public CombinedEventHandler<TickAdvancedEvent,
BuildingBlocksChangedEvent, BuildingBlocksChangedEvent,
ExpansionCostChangedEvent,
GameSpeedChangedEvent, GameSpeedChangedEvent,
BossWaveUpdatedEvent, BossWaveUpdatedEvent,
ArtifactCountChangedEvent> ArtifactCountChangedEvent>
@@ -34,16 +36,25 @@ private slots:
private: private:
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override; void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override; void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
void handleEvent(std::shared_ptr<const ExpansionCostChangedEvent> event) override;
void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override; void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override;
void handleEvent(std::shared_ptr<const BossWaveUpdatedEvent> event) override; void handleEvent(std::shared_ptr<const BossWaveUpdatedEvent> event) override;
void handleEvent(std::shared_ptr<const ArtifactCountChangedEvent> event) override; void handleEvent(std::shared_ptr<const ArtifactCountChangedEvent> 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_timeLabel;
QLabel* m_blocksLabel; QLabel* m_blocksLabel;
QLabel* m_artifactsLabel; QLabel* m_artifactsLabel;
QLabel* m_bossLabel; QLabel* m_bossLabel;
QPushButton* m_expandButton;
std::vector<QPushButton*> m_speedButtons; std::vector<QPushButton*> m_speedButtons;
int m_blocks = 0;
int m_expansionCost = 0;
static const double kSpeeds[]; static const double kSpeeds[];
static const int kSpeedCount; static const int kSpeedCount;
}; };