Compare commits
3 Commits
10c5ad678f
...
282ace4c11
| Author | SHA1 | Date | |
|---|---|---|---|
| 282ace4c11 | |||
| 1ea1cc59fb | |||
| 123c544423 |
@@ -349,27 +349,30 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
The screen is divided into three vertical sections:
|
The screen is divided into two columns: a main column (75% width) containing the header bar and game world, and a side panel column (25% width) containing the three UI panels stacked vertically:
|
||||||
|
|
||||||
```
|
```
|
||||||
+--------------------------------------------------+
|
+--------------------------------------+--------------+
|
||||||
| Header Bar |
|
| Header Bar | |
|
||||||
+--------------------------------------------------+
|
+--------------------------------------+ Selected |
|
||||||
| |
|
| | Building |
|
||||||
| Game World (70%) |
|
| | Panel |
|
||||||
| |
|
| +--------------+
|
||||||
+-----------------+-----------------+--------------+
|
| Game World | Build |
|
||||||
| Selected | Build Button | Blueprint |
|
| | Button |
|
||||||
| Building Panel | Grid | Panel |
|
| | Grid |
|
||||||
| (left) | (center) | (right) |
|
| +--------------+
|
||||||
+-----------------+-----------------+--------------+
|
| | Blueprint |
|
||||||
|
| | Panel |
|
||||||
|
+--------------------------------------+--------------+
|
||||||
|
(75% width) (25% width)
|
||||||
```
|
```
|
||||||
|
|
||||||
- REQ-UI-HEADER: The header bar spans the full width above the game world and always shows the elapsed survival time and the current global building blocks stock 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 and the current global building blocks stock 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-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-WORLD-HEIGHT: The game world view occupies 70% of the remaining screen height below the header bar.
|
- 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-HEIGHT: The UI panel occupies the remaining 30% of the screen height, split horizontally into a selected building panel (left), a build button grid (center), and a blueprint panel (right).
|
- 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).
|
||||||
|
|
||||||
### Game World
|
### Game World
|
||||||
|
|
||||||
@@ -394,6 +397,9 @@ The screen is divided into three vertical sections:
|
|||||||
- REQ-UI-DEBUG-OVERLAY: While debug draw mode is active (REQ-UI-DEBUG-DRAW), a text overlay is drawn in the upper left corner of the game world view. The overlay has a semi-transparent black background sized to fit its content. It displays the following lines of text:
|
- REQ-UI-DEBUG-OVERLAY: While debug draw mode is active (REQ-UI-DEBUG-DRAW), a text overlay is drawn in the upper left corner of the game world view. The overlay has a semi-transparent black background sized to fit its content. It displays the following lines of text:
|
||||||
- `Accumulated Threat Level: <level>` — where `<level>` is the current accumulated threat level (see REQ-WAV-THREAT-RATE).
|
- `Accumulated Threat Level: <level>` — where `<level>` is the current accumulated threat level (see REQ-WAV-THREAT-RATE).
|
||||||
- `Time until Wave: <time_s>` — where `<time_s>` is the remaining time in seconds on the normal-wave inter-wave gap timer (see REQ-WAV-GAP). During a quiet window the gap timer is frozen; the displayed value reflects that frozen state.
|
- `Time until Wave: <time_s>` — where `<time_s>` is the remaining time in seconds on the normal-wave inter-wave gap timer (see REQ-WAV-GAP). During a quiet window the gap timer is frozen; the displayed value reflects that frozen state.
|
||||||
|
- `Threat Accumulation Rate: <rate> threat/s` — the rate at which the accumulated threat level is currently increasing (see REQ-WAV-THREAT-RATE). During a quiet window (REQ-WAV-QUIET), this is 0, reflecting that accumulation is currently paused.
|
||||||
|
- `Max Factory Production: <rate> threat/s` — the threat-equivalent of the factory's total possible production: 1 threat/second for each completed (operational, not under construction) miner, smelter, assembler, reprocessing plant, and shipyard. One second of production equals one threat (see REQ-MOD-THREAT).
|
||||||
|
- `Current Factory Production: <rate> threat/s` — the threat-equivalent of the factory's current production: 1 threat/second for each completed miner, smelter, assembler, reprocessing plant, or shipyard that currently has an active production cycle (see REQ-MAT-CYCLE; for shipyards, an in-progress production cycle per REQ-BLD-SHIPYARD).
|
||||||
|
|
||||||
### Escape Menu
|
### Escape Menu
|
||||||
|
|
||||||
|
|||||||
@@ -756,13 +756,13 @@ void BeltSystem::routeSplitterItems()
|
|||||||
else if (preferA && !st.frontB)
|
else if (preferA && !st.frontB)
|
||||||
{
|
{
|
||||||
// Preferred (A) is full — fall back to B; nextOutputIsA stays.
|
// Preferred (A) is full — fall back to B; nextOutputIsA stays.
|
||||||
st.frontB = BeltItemSlot{item, 0.0};
|
st.frontB = BeltItemSlot{item, 0.75};
|
||||||
routed = true;
|
routed = true;
|
||||||
}
|
}
|
||||||
else if (!preferA && !st.frontA)
|
else if (!preferA && !st.frontA)
|
||||||
{
|
{
|
||||||
// Preferred (B) is full — fall back to A; nextOutputIsA stays.
|
// Preferred (B) is full — fall back to A; nextOutputIsA stays.
|
||||||
st.frontA = BeltItemSlot{item, 0.0};
|
st.frontA = BeltItemSlot{item, 0.75};
|
||||||
routed = true;
|
routed = true;
|
||||||
}
|
}
|
||||||
// else both fronts occupied — back stays.
|
// else both fronts occupied — back stays.
|
||||||
@@ -963,3 +963,4 @@ void BeltSystem::forEachVisualItem(QRect viewportTiles,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -867,6 +867,44 @@ std::vector<ConstructionSite> BuildingSystem::allSites() const
|
|||||||
m_constructionQueue.end());
|
m_constructionQueue.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool isProductionBuildingType(BuildingType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case BuildingType::Miner:
|
||||||
|
case BuildingType::Smelter:
|
||||||
|
case BuildingType::Assembler:
|
||||||
|
case BuildingType::ReprocessingPlant:
|
||||||
|
case BuildingType::Shipyard:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int BuildingSystem::productionBuildingCount() const
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (const Building& b : m_buildings)
|
||||||
|
{
|
||||||
|
if (isProductionBuildingType(b.type)) { ++count; }
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int BuildingSystem::activeProductionBuildingCount() const
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (const Building& b : m_buildings)
|
||||||
|
{
|
||||||
|
if (isProductionBuildingType(b.type) && b.production.has_value()) { ++count; }
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const
|
std::vector<BuildingSystem::BeltTileInfo> BuildingSystem::allBeltTiles() const
|
||||||
{
|
{
|
||||||
std::vector<BeltTileInfo> result;
|
std::vector<BeltTileInfo> result;
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ public:
|
|||||||
const ConstructionSite* findSite(BuildingId id) const;
|
const ConstructionSite* findSite(BuildingId id) const;
|
||||||
std::vector<Building> allBuildings() const;
|
std::vector<Building> allBuildings() const;
|
||||||
std::vector<ConstructionSite> allSites() const;
|
std::vector<ConstructionSite> allSites() const;
|
||||||
|
|
||||||
|
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
|
||||||
|
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
|
||||||
|
int productionBuildingCount() const;
|
||||||
|
|
||||||
|
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
|
||||||
|
// that currently has an active production cycle.
|
||||||
|
int activeProductionBuildingCount() const;
|
||||||
std::vector<BeltTileInfo> allBeltTiles() const;
|
std::vector<BeltTileInfo> allBeltTiles() const;
|
||||||
bool isTileOccupied(QPoint tile) const;
|
bool isTileOccupied(QPoint tile) const;
|
||||||
|
|
||||||
|
|||||||
@@ -863,6 +863,21 @@ double Simulation::threatLevel() const
|
|||||||
return m_waveSystem->threatLevel();
|
return m_waveSystem->threatLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double Simulation::threatAccumulationRate() const
|
||||||
|
{
|
||||||
|
return m_waveSystem->threatAccumulationRate();
|
||||||
|
}
|
||||||
|
|
||||||
|
double Simulation::maxFactoryProductionThreatRate() const
|
||||||
|
{
|
||||||
|
return static_cast<double>(m_buildingSystem->productionBuildingCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
double Simulation::currentFactoryProductionThreatRate() const
|
||||||
|
{
|
||||||
|
return static_cast<double>(m_buildingSystem->activeProductionBuildingCount());
|
||||||
|
}
|
||||||
|
|
||||||
int Simulation::bossWaveCounter() const
|
int Simulation::bossWaveCounter() const
|
||||||
{
|
{
|
||||||
return m_waveSystem->bossWaveCounter();
|
return m_waveSystem->bossWaveCounter();
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ public:
|
|||||||
int buildingBlocksStock() const;
|
int buildingBlocksStock() const;
|
||||||
bool isGameOver() const;
|
bool isGameOver() const;
|
||||||
double threatLevel() const;
|
double threatLevel() const;
|
||||||
|
double threatAccumulationRate() const;
|
||||||
|
double maxFactoryProductionThreatRate() const;
|
||||||
|
double currentFactoryProductionThreatRate() const;
|
||||||
int bossWaveCounter() const;
|
int bossWaveCounter() const;
|
||||||
Tick bossCountdownTicks() const;
|
Tick bossCountdownTicks() const;
|
||||||
Tick normalGapRemainingTicks() const;
|
Tick normalGapRemainingTicks() const;
|
||||||
|
|||||||
@@ -103,6 +103,16 @@ double WaveSystem::threatLevel() const
|
|||||||
return m_threatLevel;
|
return m_threatLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double WaveSystem::threatAccumulationRate() const
|
||||||
|
{
|
||||||
|
if (isInQuietWindow())
|
||||||
|
{
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
const double x = static_cast<double>(m_bossWaveCounter);
|
||||||
|
return std::max(0.0, m_config.world.waves.threatRateFormula.evaluate(x));
|
||||||
|
}
|
||||||
|
|
||||||
int WaveSystem::generation() const
|
int WaveSystem::generation() const
|
||||||
{
|
{
|
||||||
return m_generation;
|
return m_generation;
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ public:
|
|||||||
|
|
||||||
double threatLevel() const;
|
double threatLevel() const;
|
||||||
|
|
||||||
|
// Current rate at which threatLevel() is increasing, in threat/second
|
||||||
|
// (REQ-WAV-THREAT-RATE). 0 during a quiet window (REQ-WAV-QUIET) or when
|
||||||
|
// the rate formula evaluates to a negative value.
|
||||||
|
double threatAccumulationRate() const;
|
||||||
|
|
||||||
// Current enemy-station generation level (0 for initial set,
|
// Current enemy-station generation level (0 for initial set,
|
||||||
// incremented by 1 after each push — REQ-PSH-STATION-STATS).
|
// incremented by 1 after each push — REQ-PSH-STATION-STATS).
|
||||||
int generation() const;
|
int generation() const;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
@@ -553,6 +555,64 @@ TEST_CASE("BeltSystem: splitter falls back to other output when preferred is blo
|
|||||||
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BeltSystem: splitter fallback enters the open output at progress 0.75", "[belt]")
|
||||||
|
{
|
||||||
|
// When the preferred output is blocked, the diverted item is dropped onto the
|
||||||
|
// open output near its edge (progress 0.75) instead of at progress 0.0. This
|
||||||
|
// closes the large gap that would otherwise appear between items leaving the
|
||||||
|
// open side of a half-blocked splitter.
|
||||||
|
//
|
||||||
|
// Progress/tick = 0.25 so the 0.0-vs-0.75 entry position is observable: a
|
||||||
|
// normally-routed item starts at 0.0, a fallback item starts at 0.75.
|
||||||
|
const double quarterSpeed = 0.25 * static_cast<double>(kTickRateHz);
|
||||||
|
BeltSystem bs(quarterSpeed);
|
||||||
|
|
||||||
|
const QPoint tileSpl(1, 0);
|
||||||
|
const QPoint tileB(1, 1); // South output belt; North output has no belt (blocked).
|
||||||
|
|
||||||
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
||||||
|
bs.placeBelt(tileB, Rotation::South);
|
||||||
|
|
||||||
|
// Reads a named item's progress along the South output via the rendering contract.
|
||||||
|
// slotWorldPos maps a South-bound slot on tileSpl (y = 0) to worldPos.y == progress.
|
||||||
|
// Matching by id avoids the blocked North item, which also renders at worldPos.y 0.
|
||||||
|
auto southProgressOf = [&bs](const std::string& id) -> std::optional<double>
|
||||||
|
{
|
||||||
|
std::optional<double> progress;
|
||||||
|
bs.forEachVisualItem(QRect(-5, -5, 20, 20), [&](VisualItem vi)
|
||||||
|
{
|
||||||
|
if (vi.type.id == id)
|
||||||
|
{
|
||||||
|
progress = vi.worldPos.y();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return progress;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Permanently block output A: route one item to frontA where it sticks at 1.0
|
||||||
|
// (North has no downstream tile, so it can never move out).
|
||||||
|
bs.tryPutItem(tileSpl, makeItem("blockA"));
|
||||||
|
bs.tick(); // back: 0.25
|
||||||
|
bs.tick(); // back: 0.5 -> frontA at 0.0 (preferred A), nextOutputIsA = false
|
||||||
|
bs.tick(); bs.tick(); bs.tick(); bs.tick(); // frontA: 0.25 -> 0.5 -> 0.75 -> 1.0 (stuck)
|
||||||
|
|
||||||
|
// Item routed to B as the *preferred* output enters at progress 0.0.
|
||||||
|
bs.tryPutItem(tileSpl, makeItem("toB_pref"));
|
||||||
|
bs.tick(); // back: 0.25
|
||||||
|
bs.tick(); // back: 0.5 -> frontB at 0.0 (preferred B), nextOutputIsA = true
|
||||||
|
REQUIRE(southProgressOf("toB_pref") == Approx(0.0));
|
||||||
|
|
||||||
|
// Let it traverse and hand off to the downstream belt, freeing frontB.
|
||||||
|
bs.tick(); bs.tick(); bs.tick(); bs.tick(); // frontB: 0.25 -> 0.5 -> 0.75 -> 1.0 -> tileB
|
||||||
|
|
||||||
|
// Next item prefers A again (nextOutputIsA == true), but A is still blocked,
|
||||||
|
// so it falls back to B — and must enter near the edge at progress 0.75.
|
||||||
|
bs.tryPutItem(tileSpl, makeItem("toB_fallback"));
|
||||||
|
bs.tick(); // back: 0.25
|
||||||
|
bs.tick(); // back: 0.5 -> fallback routes to frontB at 0.75
|
||||||
|
REQUIRE(southProgressOf("toB_fallback") == Approx(0.75));
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Splitter — direct building input (no output belts)
|
// Splitter — direct building input (no output belts)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -337,6 +337,85 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
|||||||
REQUIRE_FALSE(b->production.has_value());
|
REQUIRE_FALSE(b->production.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// REQ-UI-DEBUG-OVERLAY production counts
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", "[building]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||||
|
int stock = 0;
|
||||||
|
std::mt19937 rng(0);
|
||||||
|
BuildingId nextBuildingId = 1;
|
||||||
|
BuildingSystem bs(cfg, belts,
|
||||||
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
|
[&stock](int n) { stock += n; },
|
||||||
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
|
rng);
|
||||||
|
|
||||||
|
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
|
const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0);
|
||||||
|
(void)smelterId;
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
// Both still under construction.
|
||||||
|
REQUIRE(bs.productionBuildingCount() == 0);
|
||||||
|
|
||||||
|
// The queue builds one at a time: miner (10s) completes at tick 300, then
|
||||||
|
// the smelter (15s) starts and completes at tick 300 + 450 = 750.
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||||
|
REQUIRE(bs.productionBuildingCount() == 1);
|
||||||
|
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)), tick);
|
||||||
|
REQUIRE(bs.productionBuildingCount() == 2);
|
||||||
|
|
||||||
|
// Neither has a recipe selected, so neither has an active cycle.
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
||||||
|
|
||||||
|
bs.setRecipe(minerId, "mine_iron_ore");
|
||||||
|
runTicks(bs, belts, 1, tick);
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state",
|
||||||
|
"[building]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||||
|
int stock = 0;
|
||||||
|
std::mt19937 rng(0);
|
||||||
|
BuildingId nextBuildingId = 1;
|
||||||
|
BuildingSystem bs(cfg, belts,
|
||||||
|
[&nextBuildingId]() { return nextBuildingId++; },
|
||||||
|
[&stock](int n) { stock += n; },
|
||||||
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
|
[](const std::string&) -> bool { return true; },
|
||||||
|
rng);
|
||||||
|
|
||||||
|
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
|
||||||
|
bs.setRecipe(id, "mine_iron_ore");
|
||||||
|
|
||||||
|
Tick tick = 0;
|
||||||
|
// Not yet operational while under construction.
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
||||||
|
|
||||||
|
// Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330).
|
||||||
|
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 1);
|
||||||
|
|
||||||
|
// Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the
|
||||||
|
// output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER).
|
||||||
|
runTicks(bs, belts, 2 * static_cast<int>(secondsToTicks(1.0)) + 1, tick);
|
||||||
|
|
||||||
|
const Building* b = bs.findBuilding(id);
|
||||||
|
REQUIRE(b != nullptr);
|
||||||
|
REQUIRE(static_cast<int>(b->outputBuffer.items.size()) == 2);
|
||||||
|
REQUIRE_FALSE(b->production.has_value());
|
||||||
|
REQUIRE(bs.activeProductionBuildingCount() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Belt pull → input buffer
|
// Belt pull → input buffer
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -51,6 +51,35 @@ TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
|
|||||||
REQUIRE(ws.threatLevel() == Approx(1.0));
|
REQUIRE(ws.threatLevel() == Approx(1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside quiet windows",
|
||||||
|
"[wave]")
|
||||||
|
{
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
std::mt19937 rng(42);
|
||||||
|
WaveSystem ws(cfg, rng);
|
||||||
|
|
||||||
|
// threat_rate_formula = "x", boss wave counter starts at 1 → rate = 1 threat/s.
|
||||||
|
REQUIRE(ws.threatAccumulationRate() == Approx(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wave]")
|
||||||
|
{
|
||||||
|
GameConfig cfg = loadConfig();
|
||||||
|
// Start with the boss countdown already at the pre-boss quiet threshold.
|
||||||
|
cfg.world.waves.bossCountdownSeconds = cfg.world.waves.bossQuietBeforeSeconds;
|
||||||
|
std::mt19937 rng(42);
|
||||||
|
WaveSystem ws(cfg, rng);
|
||||||
|
|
||||||
|
REQUIRE(ws.threatAccumulationRate() == Approx(0.0));
|
||||||
|
|
||||||
|
const double before = ws.threatLevel();
|
||||||
|
for (int i = 0; i < static_cast<int>(secondsToTicks(1.0)); ++i)
|
||||||
|
{
|
||||||
|
ws.tickThreatAccumulation();
|
||||||
|
}
|
||||||
|
REQUIRE(ws.threatLevel() == Approx(before));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]")
|
TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPen>
|
#include <QPen>
|
||||||
#include <QPolygonF>
|
#include <QPolygonF>
|
||||||
|
#include <QStringList>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
@@ -924,10 +925,18 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
|
|||||||
{
|
{
|
||||||
painter.resetTransform();
|
painter.resetTransform();
|
||||||
|
|
||||||
const QString line1 = tr("Accumulated Threat Level: %1")
|
const QStringList lines = {
|
||||||
.arg(m_sim->threatLevel(), 0, 'f', 1);
|
tr("Accumulated Threat Level: %1")
|
||||||
const QString line2 = tr("Time until Wave: %1s")
|
.arg(m_sim->threatLevel(), 0, 'f', 1),
|
||||||
.arg(ticksToSeconds(m_sim->normalGapRemainingTicks()), 0, 'f', 1);
|
tr("Time until Wave: %1s")
|
||||||
|
.arg(ticksToSeconds(m_sim->normalGapRemainingTicks()), 0, 'f', 1),
|
||||||
|
tr("Threat Accumulation Rate: %1 threat/s")
|
||||||
|
.arg(m_sim->threatAccumulationRate(), 0, 'f', 1),
|
||||||
|
tr("Max Factory Production: %1 threat/s")
|
||||||
|
.arg(m_sim->maxFactoryProductionThreatRate(), 0, 'f', 1),
|
||||||
|
tr("Current Factory Production: %1 threat/s")
|
||||||
|
.arg(m_sim->currentFactoryProductionThreatRate(), 0, 'f', 1),
|
||||||
|
};
|
||||||
|
|
||||||
QFont font = painter.font();
|
QFont font = painter.font();
|
||||||
font.setPointSize(m_visuals->toast.fontSize);
|
font.setPointSize(m_visuals->toast.fontSize);
|
||||||
@@ -937,19 +946,26 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
|
|||||||
const int lineH = fm.height();
|
const int lineH = fm.height();
|
||||||
const int padding = 8;
|
const int padding = 8;
|
||||||
const int spacing = 4;
|
const int spacing = 4;
|
||||||
const int textW = std::max(fm.horizontalAdvance(line1),
|
|
||||||
fm.horizontalAdvance(line2));
|
int textW = 0;
|
||||||
|
for (const QString& line : lines)
|
||||||
|
{
|
||||||
|
textW = std::max(textW, fm.horizontalAdvance(line));
|
||||||
|
}
|
||||||
const int bgW = textW + padding * 2;
|
const int bgW = textW + padding * 2;
|
||||||
const int bgH = lineH * 2 + spacing + padding * 2;
|
const int bgH = lineH * lines.size() + spacing * (lines.size() - 1) + padding * 2;
|
||||||
|
|
||||||
const QRect bgRect(padding, padding, bgW, bgH);
|
const QRect bgRect(padding, padding, bgW, bgH);
|
||||||
painter.fillRect(bgRect, QColor(0, 0, 0, 160));
|
painter.fillRect(bgRect, QColor(0, 0, 0, 160));
|
||||||
|
|
||||||
painter.setPen(Qt::white);
|
painter.setPen(Qt::white);
|
||||||
const QRect textRect1(padding * 2, padding + padding, textW, lineH);
|
int y = padding * 2;
|
||||||
const QRect textRect2(padding * 2, textRect1.bottom() + spacing, textW, lineH);
|
for (const QString& line : lines)
|
||||||
painter.drawText(textRect1, Qt::AlignLeft | Qt::AlignVCenter, line1);
|
{
|
||||||
painter.drawText(textRect2, Qt::AlignLeft | Qt::AlignVCenter, line2);
|
const QRect textRect(padding * 2, y, textW, lineH);
|
||||||
|
painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, line);
|
||||||
|
y += lineH + spacing;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::drawBeams(QPainter& painter)
|
void GameWorldView::drawBeams(QPainter& painter)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCloseEvent>
|
#include <QCloseEvent>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
@@ -40,18 +39,18 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* p
|
|||||||
|
|
||||||
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, this);
|
m_gameWorldView = new GameWorldView(sim, &sim->config(), &m_visuals, this);
|
||||||
|
|
||||||
m_bottomPanel = new QWidget(this);
|
m_sidePanel = new QWidget(this);
|
||||||
QHBoxLayout* bottomLayout = new QHBoxLayout(m_bottomPanel);
|
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
|
||||||
bottomLayout->setContentsMargins(0, 0, 0, 0);
|
sideLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
bottomLayout->setSpacing(0);
|
sideLayout->setSpacing(0);
|
||||||
|
|
||||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_bottomPanel);
|
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->config(), m_sidePanel);
|
||||||
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_bottomPanel);
|
m_buildButtonGrid = new BuildButtonGrid(&sim->config(), m_sidePanel);
|
||||||
m_blueprintPanel = new BlueprintPanel(sim, &sim->config(), m_bottomPanel);
|
m_blueprintPanel = new BlueprintPanel(sim, &sim->config(), m_sidePanel);
|
||||||
|
|
||||||
bottomLayout->addWidget(m_selectedBuildingPanel, 1);
|
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||||
bottomLayout->addWidget(m_buildButtonGrid, 1);
|
sideLayout->addWidget(m_buildButtonGrid, 1);
|
||||||
bottomLayout->addWidget(m_blueprintPanel, 1);
|
sideLayout->addWidget(m_blueprintPanel, 1);
|
||||||
|
|
||||||
m_gameWorldView->setFocus();
|
m_gameWorldView->setFocus();
|
||||||
|
|
||||||
@@ -117,13 +116,12 @@ void MainWindow::layoutPanels()
|
|||||||
const int totalH = height();
|
const int totalH = height();
|
||||||
const int headerH = m_headerBar->sizeHint().height();
|
const int headerH = m_headerBar->sizeHint().height();
|
||||||
if (headerH <= 0) { return; }
|
if (headerH <= 0) { return; }
|
||||||
const int remaining = totalH - headerH;
|
const int mainW = totalW * 75 / 100;
|
||||||
const int gameH = remaining * 70 / 100;
|
const int sideW = totalW - mainW;
|
||||||
const int panelH = remaining - gameH;
|
|
||||||
|
|
||||||
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
m_headerBar->setGeometry(0, 0, mainW, headerH);
|
||||||
m_gameWorldView->setGeometry(0, headerH, totalW, gameH);
|
m_gameWorldView->setGeometry(0, headerH, mainW, totalH - headerH);
|
||||||
m_bottomPanel->setGeometry(0, headerH + gameH, totalW, panelH);
|
m_sidePanel->setGeometry(mainW, 0, sideW, totalH);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
void MainWindow::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ private:
|
|||||||
SelectedBuildingPanel* m_selectedBuildingPanel;
|
SelectedBuildingPanel* m_selectedBuildingPanel;
|
||||||
BuildButtonGrid* m_buildButtonGrid;
|
BuildButtonGrid* m_buildButtonGrid;
|
||||||
BlueprintPanel* m_blueprintPanel;
|
BlueprintPanel* m_blueprintPanel;
|
||||||
QWidget* m_bottomPanel;
|
QWidget* m_sidePanel;
|
||||||
|
|
||||||
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user