show threat rate in debug output
This commit is contained in:
@@ -397,6 +397,9 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
- 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).
|
||||
- `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
|
||||
|
||||
|
||||
@@ -867,6 +867,44 @@ std::vector<ConstructionSite> BuildingSystem::allSites() const
|
||||
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<BeltTileInfo> result;
|
||||
|
||||
@@ -79,6 +79,14 @@ public:
|
||||
const ConstructionSite* findSite(BuildingId id) const;
|
||||
std::vector<Building> allBuildings() 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;
|
||||
bool isTileOccupied(QPoint tile) const;
|
||||
|
||||
|
||||
@@ -863,6 +863,21 @@ double Simulation::threatLevel() const
|
||||
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
|
||||
{
|
||||
return m_waveSystem->bossWaveCounter();
|
||||
|
||||
@@ -67,6 +67,9 @@ public:
|
||||
int buildingBlocksStock() const;
|
||||
bool isGameOver() const;
|
||||
double threatLevel() const;
|
||||
double threatAccumulationRate() const;
|
||||
double maxFactoryProductionThreatRate() const;
|
||||
double currentFactoryProductionThreatRate() const;
|
||||
int bossWaveCounter() const;
|
||||
Tick bossCountdownTicks() const;
|
||||
Tick normalGapRemainingTicks() const;
|
||||
|
||||
@@ -103,6 +103,16 @@ double WaveSystem::threatLevel() const
|
||||
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
|
||||
{
|
||||
return m_generation;
|
||||
|
||||
@@ -36,6 +36,11 @@ public:
|
||||
|
||||
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,
|
||||
// incremented by 1 after each push — REQ-PSH-STATION-STATS).
|
||||
int generation() const;
|
||||
|
||||
@@ -337,6 +337,85 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -51,6 +51,35 @@ TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
|
||||
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]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <QPainter>
|
||||
#include <QPen>
|
||||
#include <QPolygonF>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
|
||||
#include "BeltSystem.h"
|
||||
@@ -924,10 +925,18 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
|
||||
{
|
||||
painter.resetTransform();
|
||||
|
||||
const QString line1 = tr("Accumulated Threat Level: %1")
|
||||
.arg(m_sim->threatLevel(), 0, 'f', 1);
|
||||
const QString line2 = tr("Time until Wave: %1s")
|
||||
.arg(ticksToSeconds(m_sim->normalGapRemainingTicks()), 0, 'f', 1);
|
||||
const QStringList lines = {
|
||||
tr("Accumulated Threat Level: %1")
|
||||
.arg(m_sim->threatLevel(), 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();
|
||||
font.setPointSize(m_visuals->toast.fontSize);
|
||||
@@ -937,19 +946,26 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
|
||||
const int lineH = fm.height();
|
||||
const int padding = 8;
|
||||
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 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);
|
||||
painter.fillRect(bgRect, QColor(0, 0, 0, 160));
|
||||
|
||||
painter.setPen(Qt::white);
|
||||
const QRect textRect1(padding * 2, padding + padding, textW, lineH);
|
||||
const QRect textRect2(padding * 2, textRect1.bottom() + spacing, textW, lineH);
|
||||
painter.drawText(textRect1, Qt::AlignLeft | Qt::AlignVCenter, line1);
|
||||
painter.drawText(textRect2, Qt::AlignLeft | Qt::AlignVCenter, line2);
|
||||
int y = padding * 2;
|
||||
for (const QString& line : lines)
|
||||
{
|
||||
const QRect textRect(padding * 2, y, textW, lineH);
|
||||
painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, line);
|
||||
y += lineH + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
void GameWorldView::drawBeams(QPainter& painter)
|
||||
|
||||
Reference in New Issue
Block a user