From df9f0d5ccd810a21ec3bba0cc9a8e9095ea5fcc7 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Sat, 4 Jul 2026 08:07:52 +0200 Subject: [PATCH] show total time in balancing target arenas --- docs/requirements.md | 6 +++--- src/balancing/ArenaSimulation.cpp | 1 + src/balancing/ArenaSimulation.h | 3 +++ src/balancing/ArenaWidget.cpp | 7 +++++++ src/balancing/ArenaWidget.h | 1 + src/balancing/BalancingWindow.cpp | 10 +++++++++- src/balancing/InspectWindow.cpp | 7 +++++++ src/balancing/InspectWindow.h | 1 + 8 files changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/requirements.md b/docs/requirements.md index dba284b..f88a69b 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -516,13 +516,13 @@ A separate executable target (`balancing`) that links against `lib` but contains - REQ-BAL-UI-RELOAD: The "Reload Config" button reloads all config files from disk (`balancing.toml`, `ships.toml`, `stations.toml`), stops any running simulations, and replaces the arena widget list with freshly created widgets from the reloaded config. The button is disabled while any arena simulation is currently running. - REQ-BAL-UI-START-ALL: The "Start All" button is placed above the scrollable arena list, to the right of the "Reload Config" button. Clicking it starts (or restarts) the simulation for every arena that is not currently running. The button is disabled when all arenas are currently running. - REQ-BAL-UI-LOG: The "Log" button is placed in the header to the right of the "Start All" button. It is enabled whenever the main window's controls are enabled — including while simulations are running (it captures a live snapshot) — and, like all main window controls, is disabled only while an arena is being inspected (REQ-BAL-UI-WINDOW). Clicking it writes the current state of every arena to a file named `balancing_log.md` in the tool's current working directory, replacing (overwriting) any previous content of that file. The log captures, for every arena in `balancing.toml` order, exactly the information shown in that arena's widget (REQ-BAL-UI-WIDGET) at the moment the button is clicked. Each arena is written as its own section: - - A heading with the arena name followed by the arena's current state — `not started`, `running`, or `ended` (corresponding to the widget border colors of REQ-BAL-UI-WIDGET-BORDER). + - A heading with the arena name followed by the arena's current state — `not started`, `running`, or `ended` (corresponding to the widget border colors of REQ-BAL-UI-WIDGET-BORDER). For an `ended` arena, the heading also includes the battle duration (REQ-BAL-UI-WIDGET), for example `ended, 42.3 s`. - A markdown table with one column per team (team 1 left, team 2 right). Each team's column header shows the team name — prefixed with `[WON]` when that team won, matching REQ-BAL-UI-WIDGET — the team's accumulated threat level, and the team's remaining EHP percentage (REQ-BAL-UI-WIDGET). - Below the header, each table row holds one of that team's entries, in the same order and text format as the widget (REQ-BAL-UI-WIDGET): the HQ first, then ship types, then defence stations, formatted `surviving/total TypeName` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries. When the two teams have different numbers of entries, the shorter column's remaining cells are left blank. -- REQ-BAL-UI-WIDGET: Each arena widget displays the arena name, an "Inspect" button (to the right of the arena name), and two columns (one per team). Each column shows the team name as a header, then directly below the header the team's **accumulated threat level** — the sum, across the team's configured ship entries, of each entry's `count` multiplied by the threat cost (REQ-MOD-THREAT) of one ship of that entry computed from its level-independent module layout. Only ships contribute; the HQ and defence stations are excluded. This value is static: it is computed once from the full configured roster and does not change as ships are destroyed during the fight. Directly below the threat level, the column shows the team's **remaining EHP percentage** — the sum of the current HP of all of the team's ships and defence stations, divided by the sum of their maximum HP, expressed as a percentage rounded to a whole number. Maximum HP is the final per-entity maximum (REQ-MOD-STAT-CALC), so module HP bonuses such as armor plates are included. (The game has no damage mitigation, so effective HP equals raw HP.) The HQ is excluded from both sums. A destroyed ship or station contributes 0 to the numerator and its maximum HP to the denominator, so the value measures how much of the team's fielded durability remains: it starts at 100% and decreases as units take damage or are destroyed. Unlike the static threat level, this value updates live as the simulation progresses. If the team has neither ships nor defence stations (the denominator is 0), the percentage is shown as `n/a`. Below the EHP percentage, the column shows a list of entries. The HQ is always the first entry in each column. Below the HQ, ship types are listed, followed by defence stations (if any). Each entry uses the format `surviving/total TypeName` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries — for example `2/3 Fighter`, `1/1 HQ L1`, or `2/2 Enemy Station L3`. The surviving count updates live as the simulation progresses. When the fight ends, the winning team's name header is prefixed with `[WON]`. +- REQ-BAL-UI-WIDGET: Each arena widget displays the arena name, an "Inspect" button (to the right of the arena name), and two columns (one per team). Each column shows the team name as a header, then directly below the header the team's **accumulated threat level** — the sum, across the team's configured ship entries, of each entry's `count` multiplied by the threat cost (REQ-MOD-THREAT) of one ship of that entry computed from its level-independent module layout. Only ships contribute; the HQ and defence stations are excluded. This value is static: it is computed once from the full configured roster and does not change as ships are destroyed during the fight. Directly below the threat level, the column shows the team's **remaining EHP percentage** — the sum of the current HP of all of the team's ships and defence stations, divided by the sum of their maximum HP, expressed as a percentage rounded to a whole number. Maximum HP is the final per-entity maximum (REQ-MOD-STAT-CALC), so module HP bonuses such as armor plates are included. (The game has no damage mitigation, so effective HP equals raw HP.) The HQ is excluded from both sums. A destroyed ship or station contributes 0 to the numerator and its maximum HP to the denominator, so the value measures how much of the team's fielded durability remains: it starts at 100% and decreases as units take damage or are destroyed. Unlike the static threat level, this value updates live as the simulation progresses. If the team has neither ships nor defence stations (the denominator is 0), the percentage is shown as `n/a`. Below the EHP percentage, the column shows a list of entries. The HQ is always the first entry in each column. Below the HQ, ship types are listed, followed by defence stations (if any). Each entry uses the format `surviving/total TypeName` for ship entries and `surviving/total TypeName Llevel` for the HQ and defence station entries — for example `2/3 Fighter`, `1/1 HQ L1`, or `2/2 Enemy Station L3`. The surviving count updates live as the simulation progresses. When the fight ends, the winning team's name header is prefixed with `[WON]`. When the fight has ended, the widget also displays the arena's **battle duration** — an arena-level value (not per team) giving the game time the fight lasted, computed as the number of simulated ticks at completion multiplied by the simulation's fixed tick duration (the same tick-based simulation as the main game, REQ-BAL-SIM-ENV). This is game time, not wall-clock time, so it is independent of how fast the arena was simulated (non-inspected arenas run at maximum tick rate, REQ-BAL-SIM-SPEED). It is shown in seconds with one decimal place, for example `Duration: 42.3 s`. The battle duration is shown only for completed (ended) runs; it is not shown while the arena is not started or running. - REQ-BAL-UI-WIDGET-START: Each arena widget contains a "Start" button that starts the simulation for that arena. The button is disabled while the arena's simulation is running. When a finished arena's Start button is clicked, a fresh simulation is created and started (the widget resets to initial unit counts, the border returns to blue, and the previous results are replaced). - REQ-BAL-UI-WIDGET-BORDER: Each arena widget has a colored border indicating its state: grey when not yet started, blue while its simulation is running, and green when the fight has ended. - REQ-BAL-UI-INSPECT: Clicking an arena widget's "Inspect" button opens a new inspect window for that arena. Any previously open inspect window is closed first (its arena's simulation is aborted and its widget border returns to grey). The inspected arena is restarted with a fresh simulation that runs at controllable game speed with full rendering (REQ-BAL-SIM-SPEED). The arena widget updates live during inspection (surviving counts, border color, `[WON]` prefix) as it does for non-inspected arenas. Only one inspect window may be open at a time. -- REQ-BAL-UI-INSPECT-WINDOW: The inspect window consists of three sections, top to bottom: a title bar area containing the arena name and game speed controls (same buttons as the main game: 0×, 0.5×, 1×, 2×, 4×, with Space to toggle pause — see REQ-UI-SPEED and REQ-UI-HOTKEYS), the arena view in the center, and an info panel at the bottom displaying the same team columns and entry format as the arena widget in the main window (REQ-BAL-UI-WIDGET), updated live. +- REQ-BAL-UI-INSPECT-WINDOW: The inspect window consists of three sections, top to bottom: a title bar area containing the arena name and game speed controls (same buttons as the main game: 0×, 0.5×, 1×, 2×, 4×, with Space to toggle pause — see REQ-UI-SPEED and REQ-UI-HOTKEYS), the arena view in the center, and an info panel at the bottom displaying the same team columns and entry format as the arena widget in the main window (REQ-BAL-UI-WIDGET), updated live, including the arena's battle duration once the fight has ended (REQ-BAL-UI-WIDGET). - REQ-BAL-UI-INSPECT-VIEW: The arena view renders all tiles of the arena and displays ships, HQs, defence stations, and laser beams using the same visual elements and `visuals.toml` colors as the main game. Team 1 uses player visual styles; team 2 uses enemy visual styles. The view has a fixed zoom level — no zoom or scroll is possible. The tile size is derived so that the full arena (all tiles) fits within the view. - REQ-BAL-UI-INSPECT-CLOSE: Closing the inspect window (via the window's close button) aborts the inspected arena's simulation. The arena widget's border returns to grey and its surviving counts are left as they were at the moment of closing. All main window buttons and controls are re-enabled. diff --git a/src/balancing/ArenaSimulation.cpp b/src/balancing/ArenaSimulation.cpp index 6cd3ee8..ae1678c 100644 --- a/src/balancing/ArenaSimulation.cpp +++ b/src/balancing/ArenaSimulation.cpp @@ -515,6 +515,7 @@ void ArenaSimulation::updateStatus() ArenaStatus newStatus; newStatus.finished = m_finished; newStatus.winnerTeam = m_winnerTeam; + newStatus.durationSeconds = ticksToSeconds(m_currentTick); // Live remaining HP of each team's ships and defence stations (HQ excluded); // the EHP-percentage numerator (denominator is the fixed m_teamMaxEhp). diff --git a/src/balancing/ArenaSimulation.h b/src/balancing/ArenaSimulation.h index 1c5d1ad..173642e 100644 --- a/src/balancing/ArenaSimulation.h +++ b/src/balancing/ArenaSimulation.h @@ -59,6 +59,9 @@ struct ArenaStatus TeamStatus teams[2]; bool finished = false; int winnerTeam = -1; // 0 or 1 when finished; -1 while running + // Game time the fight has lasted (simulated ticks * fixed tick duration). + // Meaningful once finished; the battle duration shown for completed runs. + double durationSeconds = 0.0; }; class ArenaSimulation diff --git a/src/balancing/ArenaWidget.cpp b/src/balancing/ArenaWidget.cpp index 68dbce3..4d4b884 100644 --- a/src/balancing/ArenaWidget.cpp +++ b/src/balancing/ArenaWidget.cpp @@ -33,6 +33,9 @@ void ArenaWidget::buildLayout(const std::string& arenaName) m_titleLabel->setFont(titleFont); titleRow->addWidget(m_titleLabel); + m_durationLabel = new QLabel(this); + titleRow->addWidget(m_durationLabel); + titleRow->addStretch(); m_inspectButton = new QPushButton(tr("Inspect"), this); @@ -159,6 +162,10 @@ void ArenaWidget::updateStatus(const ArenaStatus& status) content->setText(lines); } + m_durationLabel->setText(status.finished + ? tr("Duration: %1 s").arg(QString::number(status.durationSeconds, 'f', 1)) + : QString()); + if (status.finished && !m_wasFinished) { m_wasFinished = true; diff --git a/src/balancing/ArenaWidget.h b/src/balancing/ArenaWidget.h index e62a688..655a1c0 100644 --- a/src/balancing/ArenaWidget.h +++ b/src/balancing/ArenaWidget.h @@ -36,6 +36,7 @@ private: int m_arenaIndex; ArenaStatus m_lastStatus; QLabel* m_titleLabel; + QLabel* m_durationLabel; QLabel* m_team1Header; QLabel* m_team2Header; QLabel* m_team1Threat; diff --git a/src/balancing/BalancingWindow.cpp b/src/balancing/BalancingWindow.cpp index a35e4a4..6d28935 100644 --- a/src/balancing/BalancingWindow.cpp +++ b/src/balancing/BalancingWindow.cpp @@ -373,9 +373,17 @@ void BalancingWindow::writeLog() for (const ArenaEntry& entry : m_arenas) { const ArenaStatus& status = entry.widget->getLastStatus(); + const ArenaWidget::State state = entry.widget->getState(); + + QString stateSuffix = stateText(state); + if (state == ArenaWidget::State::Ended) + { + stateSuffix += QStringLiteral(", %1 s") + .arg(QString::number(status.durationSeconds, 'f', 1)); + } out << "\n## Arena: " << QString::fromStdString(entry.config.name) - << " (" << stateText(entry.widget->getState()) << ")\n\n"; + << " (" << stateSuffix << ")\n\n"; out << "| " << teamHeaderCell(status, 0) << " | " << teamHeaderCell(status, 1) << " |\n"; diff --git a/src/balancing/InspectWindow.cpp b/src/balancing/InspectWindow.cpp index 960c4a8..b21397a 100644 --- a/src/balancing/InspectWindow.cpp +++ b/src/balancing/InspectWindow.cpp @@ -52,6 +52,9 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config, nameLabel->setFont(nameFont); headerLayout->addWidget(nameLabel); + m_durationLabel = new QLabel(header); + headerLayout->addWidget(m_durationLabel); + headerLayout->addStretch(); const char* labels[] = { "0x", "0.5x", "1x", "2x", "10x" }; @@ -202,6 +205,10 @@ void InspectWindow::pollStatus() void InspectWindow::updateInfoPanel(const ArenaStatus& status) { + m_durationLabel->setText(status.finished + ? tr("Duration: %1 s").arg(QString::number(status.durationSeconds, 'f', 1)) + : QString()); + for (int ti = 0; ti < 2; ++ti) { const ArenaStatus::TeamStatus& team = status.teams[ti]; diff --git a/src/balancing/InspectWindow.h b/src/balancing/InspectWindow.h index 5198628..ea3826b 100644 --- a/src/balancing/InspectWindow.h +++ b/src/balancing/InspectWindow.h @@ -54,6 +54,7 @@ private: ArenaView* m_arenaView; std::vector m_speedButtons; + QLabel* m_durationLabel; QLabel* m_team1Header; QLabel* m_team2Header; QLabel* m_team1Threat;