From f39fe9a118eb4555a69a4af2be84dfaaee0b7758 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Wed, 5 Aug 2026 21:27:21 +0200 Subject: [PATCH] move the debug stats panel out of the renderer --- docs/architecture.md | 2 +- src/ui/GameWorldView.cpp | 47 +++++++++++++++++++++++++++++++++++++ src/ui/GameWorldView.h | 4 ++++ src/ui/WorldRenderer.cpp | 50 ---------------------------------------- src/ui/WorldRenderer.h | 18 ++++----------- 5 files changed, 57 insertions(+), 64 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index b4572fa..61c76ee 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -331,7 +331,7 @@ Buildings and the belt subsystem stay outside any entity model regardless of wha The game world is drawn into a single `GameWorldView` widget that inherits `QOpenGLWidget` and uses `QPainter` for all drawing. This gives the same imperative paint API as a plain `QWidget` with GPU acceleration, comfortably handling the expected scale (hundreds of ships, thousands of belt items) without blocking the main thread on CPU rasterization. -The drawing itself lives in `WorldRenderer`, not in the widget. `paintGL` is a call sequence: build the frame's `WorldCoordinates`, hand the renderer a `WorldRenderFrame`, then draw the screen-anchored chrome. The split is the world-space / screen-space line: the renderer draws everything positioned in tiles, while the pause and deconstruct vignettes and the replay overlay — which are anchored to the viewport and never took a `WorldCoordinates` — stay with the widget. +The drawing itself lives in `WorldRenderer`, not in the widget. `paintGL` is a call sequence: build the frame's `WorldCoordinates`, hand the renderer a `WorldRenderFrame`, then draw the screen-anchored chrome. The split is the world-space / screen-space line, and it is exact: the renderer draws everything positioned in tiles, while everything positioned in pixels — the pause and deconstruct vignettes, the replay overlay, the debug stats panel — stays with the widget. A useful consequence is that the renderer draws no translatable text at all (its text is config-driven glyphs, ASCII port arrows, and numbers), so it needs no `tr()` and no tie to the meta-object system. `WorldRenderFrame` is what makes the renderer independent of the widget. The renderer reads the simulation directly, but everything else it draws is interaction state the widget owns — the selection, the active build mode, live beams, the copy-settings feedback, the box-select rectangle. Those are gathered into the frame each `paintGL` and passed by reference, so the renderer keeps no copy that a later click could invalidate. The renderer knows nothing about input: the widget resolves clicks and hit-tests, and the renderer only draws the result. diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 2359b40..223c267 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -405,6 +406,7 @@ void GameWorldView::paintGL() // Screen-anchored chrome over the finished world. drawScreenSpace(painter); + if (m_debugDraw) { drawDebugOverlay(painter); } drawPauseBorder(painter); drawDeconstructBorder(painter); drawReplayOverlay(painter); @@ -821,6 +823,51 @@ void GameWorldView::applyBeltDragPath() // --------------------------------------------------------------------------- +void GameWorldView::drawDebugOverlay(QPainter& painter) +{ + const QStringList lines = { + tr("Accumulated Threat Level: %1") + .arg(m_sim->getThreatLevel(), 0, 'f', 1), + tr("Time until Wave: %1s") + .arg(ticksToSeconds(m_sim->getNormalGapRemainingTicks()), 0, 'f', 1), + tr("Threat Accumulation Rate: %1 threat/s") + .arg(m_sim->getThreatAccumulationRate(), 0, 'f', 1), + tr("Max Factory Production: %1 threat/s") + .arg(m_sim->getMaxFactoryProductionThreatRate(), 0, 'f', 1), + tr("Current Factory Production: %1 threat/s") + .arg(m_sim->getCurrentFactoryProductionThreatRate(), 0, 'f', 1), + }; + + QFont font = painter.font(); + font.setPointSize(m_visuals->toast.fontSize); + painter.setFont(font); + + const QFontMetrics fm = painter.fontMetrics(); + const int lineH = fm.height(); + const int padding = 8; + const int spacing = 4; + + 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 * 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); + 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::drawScreenSpace(QPainter& /*painter*/) { } diff --git a/src/ui/GameWorldView.h b/src/ui/GameWorldView.h index 9f0ec35..da98647 100644 --- a/src/ui/GameWorldView.h +++ b/src/ui/GameWorldView.h @@ -150,6 +150,10 @@ private: // Screen-anchored chrome, drawn after the world (see WorldRenderer): these // need no world transform, which is exactly why they stayed here. void drawScreenSpace(QPainter& painter); + // Threat and production readout shown while debug draw is on (F3). Positioned + // in pixels at the top-left corner, so it belongs with the chrome rather than + // with the world. + void drawDebugOverlay(QPainter& painter); // Vignette-style border shown while the game is paused (speed 0x, REQ-UI-PAUSE-BORDER) // to make the paused state hard to miss: a black frame whose alpha fades from 50% at // the viewport edges to 0% toward the center. diff --git a/src/ui/WorldRenderer.cpp b/src/ui/WorldRenderer.cpp index a1b1cb2..5cc51bb 100644 --- a/src/ui/WorldRenderer.cpp +++ b/src/ui/WorldRenderer.cpp @@ -9,14 +9,12 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include #include "AttackBehavior.h" @@ -139,7 +137,6 @@ void WorldRenderer::render(QPainter& painter, const WorldCoordinates& coordinate { drawDebugSensorRanges(painter, coordinates, frame); drawDebugTargetLines(painter, coordinates, frame); - drawDebugOverlay(painter); } drawShips(painter, coordinates, frame); drawBeams(painter, coordinates, frame); @@ -819,53 +816,6 @@ void WorldRenderer::drawDebugTargetLines(QPainter& painter, const WorldCoordinat }); } -void WorldRenderer::drawDebugOverlay(QPainter& painter) -{ - painter.resetTransform(); - - const QStringList lines = { - tr("Accumulated Threat Level: %1") - .arg(m_sim.getThreatLevel(), 0, 'f', 1), - tr("Time until Wave: %1s") - .arg(ticksToSeconds(m_sim.getNormalGapRemainingTicks()), 0, 'f', 1), - tr("Threat Accumulation Rate: %1 threat/s") - .arg(m_sim.getThreatAccumulationRate(), 0, 'f', 1), - tr("Max Factory Production: %1 threat/s") - .arg(m_sim.getMaxFactoryProductionThreatRate(), 0, 'f', 1), - tr("Current Factory Production: %1 threat/s") - .arg(m_sim.getCurrentFactoryProductionThreatRate(), 0, 'f', 1), - }; - - QFont font = painter.font(); - font.setPointSize(m_visuals.toast.fontSize); - painter.setFont(font); - - const QFontMetrics fm = painter.fontMetrics(); - const int lineH = fm.height(); - const int padding = 8; - const int spacing = 4; - - 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 * 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); - 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 WorldRenderer::drawBeams(QPainter& painter, const WorldCoordinates& coordinates, const WorldRenderFrame& frame) { diff --git a/src/ui/WorldRenderer.h b/src/ui/WorldRenderer.h index 40e5d0d..0915df9 100644 --- a/src/ui/WorldRenderer.h +++ b/src/ui/WorldRenderer.h @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -70,16 +69,13 @@ struct WorldRenderFrame // // Reads the simulation and never writes it, and knows nothing about input — the // view resolves clicks and owns the interaction state, and hands the parts the -// renderer needs over in a WorldRenderFrame. Screen-anchored chrome that is not -// part of the world (the pause and deconstruct vignettes, the replay overlay) -// stays with the view, which is also why those never took a WorldCoordinates. +// renderer needs over in a WorldRenderFrame. +// +// Everything here is positioned in tiles. Screen-anchored chrome — the pause and +// deconstruct vignettes, the replay overlay, the debug stats panel — stays with +// the view, which is why nothing in this class draws translatable text. class WorldRenderer { - // Not a QObject, but drawDebugOverlay puts text on screen, so it still needs - // tr() for translation (REQ-UI-...): this declares the tr() overloads without - // dragging in the meta-object system. - Q_DECLARE_TR_FUNCTIONS(WorldRenderer) - public: // `itemIcons` is the window-wide per-item icon cache (REQ-UI-ITEM-ICON); not // owned, must outlive this renderer. `configDir` is used once, to load the @@ -121,10 +117,6 @@ private: const WorldRenderFrame& frame); void drawOverlays(QPainter& painter, const WorldCoordinates& coordinates, const WorldRenderFrame& frame); - // Screen-anchored, but drawn mid-world so ships and effects still paint over - // it, as they always have. - void drawDebugOverlay(QPainter& painter); - // Draws a single item centered at widget-space `center`, spanning `halfPx` in // each direction (a half-tile). Uses the item's icon when one exists // (REQ-UI-ITEM-ICON), otherwise falls back to the colored square from