diff --git a/src/test/SimulationTest.cpp b/src/test/SimulationTest.cpp index 371f160..a2ffd95 100644 --- a/src/test/SimulationTest.cpp +++ b/src/test/SimulationTest.cpp @@ -131,3 +131,26 @@ TEST_CASE("TickDriver::reset clears the accumulator", "[simulation]") // Nothing in the accumulator: zero elapsed time should not fire. REQUIRE(driver.advance(0.0, 1.0) == 0); } + +TEST_CASE("TickDriver::reset discards a large pending delta so a restart does not " + "fast-forward", "[simulation]") +{ + // Regression guard for the "restart fast-forwards by the time spent in the + // Game Over / Win / escape dialog" bug: on restart the frame timer holds the + // whole wall-clock duration the modal was open. GameWorldView::resetForNewGame() + // now calls TickDriver::reset() to discard that pending delta; without it the + // fresh run would burst forward by the dialog duration on its first frame. + TickDriver driver; + + // 30 seconds spent in the dialog would otherwise be ~900 ticks at 30 Hz. + const double dialogOpenMs = 30000.0; + driver.reset(); + + // A brand-new run's first normal frame (~16 ms) advances only its own ticks; + // the discarded dialog time contributes nothing. + REQUIRE(driver.advance(16.0, 1.0) == 0); + + // Sanity: had the dialog delta not been discarded, it would have fired ~900 ticks. + TickDriver leaked; + REQUIRE(leaked.advance(dialogOpenMs, 1.0) > 800); +} diff --git a/src/ui/GameWorldView.cpp b/src/ui/GameWorldView.cpp index 7a6cb39..0005915 100644 --- a/src/ui/GameWorldView.cpp +++ b/src/ui/GameWorldView.cpp @@ -227,6 +227,12 @@ void GameWorldView::onFrame() { m_viewResetPending = false; resetForNewGame(); + // The reset command may have drained after a long-open modal (e.g. the + // Game Over dialog), so `elapsed` above still holds the whole time that + // dialog was open. Feeding it into the tick driver would fast-forward the + // brand-new run by that duration. resetForNewGame() has rebased the time + // source; skip this frame's tick advance so the stale delta is discarded. + return; } // Notify presentation widgets that queued commands were applied, so a @@ -2391,6 +2397,12 @@ void GameWorldView::resetForNewGame() EventManager::getInstance()->sendEventImmediately( std::make_shared(std::vector{})); setGameSpeed(1.0); + // Rebase the wall-clock time source so a fresh run starts from a clean time + // base. Without this, wall time accumulated while a modal (Game Over, Win, or + // the escape menu) was open would be converted into ticks on the new run, + // fast-forwarding it by the time the player spent in the dialog. + m_frameTimer.restart(); + m_tickDriver.reset(); update(); }