diff --git a/src/app/TetrisApp.cpp b/src/app/TetrisApp.cpp index b3998df..68db98c 100644 --- a/src/app/TetrisApp.cpp +++ b/src/app/TetrisApp.cpp @@ -460,6 +460,7 @@ int TetrisApp::Impl::init() ctx.exitPopupSelectedButton = &exitPopupSelectedButton; ctx.gameplayCountdownActive = &gameplayCountdownActive; ctx.menuPlayCountdownArmed = &menuPlayCountdownArmed; + ctx.skipNextLevelUpJingle = &skipNextLevelUpJingle; ctx.challengeClearFxActive = &challengeClearFxActive; ctx.challengeClearFxElapsedMs = &challengeClearFxElapsedMs; ctx.challengeClearFxDurationMs = &challengeClearFxDurationMs; @@ -1090,7 +1091,13 @@ void TetrisApp::Impl::runLoop() const std::vector audioIds = {"clear_line","nice_combo","you_fire","well_played","keep_that_ryhtm","great_move","smooth_clear","impressive","triple_strike","amazing","you_re_unstoppable","boom_tetris","wonderful","lets_go","hard_drop","new_level","asteroid_destroy","challenge_clear"}; for (const auto &id : audioIds) { - std::string basePath = "assets/music/" + (id == "hard_drop" ? "hard_drop_001" : (id == "challenge_clear" ? "GONG0" : id)); + std::string basePath = "assets/music/" + (id == "hard_drop" + ? "hard_drop_001" + : (id == "challenge_clear" + ? "GONG0" + : (id == "asteroid_destroy" + ? "asteroid-destroy" + : id))); { std::lock_guard lk(currentLoadingMutex); currentLoadingFile = basePath; diff --git a/src/gameplay/core/Game.cpp b/src/gameplay/core/Game.cpp index bf24a4d..fe9e205 100644 --- a/src/gameplay/core/Game.cpp +++ b/src/gameplay/core/Game.cpp @@ -510,6 +510,31 @@ int Game::checkLines() { completedLines.push_back(y); } } + + // Pre-play asteroid destroy SFX immediately when a clearing line contains asteroids (reduces latency) + if (!completedLines.empty() && mode == GameMode::Challenge) { + std::optional foundType; + for (int y : completedLines) { + for (int x = 0; x < COLS; ++x) { + int idx = y * COLS + x; + if (board[idx] >= ASTEROID_BASE) { + int typeIdx = board[idx] - ASTEROID_BASE; + if (typeIdx >= 0 && typeIdx <= static_cast(AsteroidType::Core)) { + foundType = static_cast(typeIdx); + } + } else if (idx >= 0 && idx < static_cast(asteroidGrid.size()) && asteroidGrid[idx].has_value()) { + foundType = asteroidGrid[idx]->type; + } + } + } + if (foundType.has_value()) { + pendingAsteroidDestroyType = foundType; + if (!asteroidDestroySoundPreplayed && asteroidDestroyedCallback) { + asteroidDestroySoundPreplayed = true; + asteroidDestroyedCallback(*foundType); + } + } + } return static_cast(completedLines.size()); } @@ -537,6 +562,10 @@ void Game::actualClearLines() { // Apply asteroid-specific gravity after the board collapses applyAsteroidGravity(); + // Reset preplay latch so future destroys can fire again + pendingAsteroidDestroyType.reset(); + asteroidDestroySoundPreplayed = false; + if (mode == GameMode::Challenge) { if (asteroidsRemainingCount <= 0) { int nextLevel = challengeLevelIndex + 1; @@ -629,7 +658,7 @@ void Game::handleAsteroidsOnClearedRows(const std::vector& clearedRows, if (destroyedThisPass > 0) { asteroidsRemainingCount = std::max(0, asteroidsRemainingCount - destroyedThisPass); - if (asteroidDestroyedCallback && lastDestroyedType.has_value()) { + if (!asteroidDestroySoundPreplayed && asteroidDestroyedCallback && lastDestroyedType.has_value()) { asteroidDestroyedCallback(*lastDestroyedType); } } diff --git a/src/gameplay/core/Game.h b/src/gameplay/core/Game.h index 57587c2..c285f99 100644 --- a/src/gameplay/core/Game.h +++ b/src/gameplay/core/Game.h @@ -177,6 +177,9 @@ private: bool challengeLevelActive{false}; bool challengeAdvanceQueued{false}; int challengeQueuedLevel{0}; + // Asteroid SFX latency mitigation + std::optional pendingAsteroidDestroyType; + bool asteroidDestroySoundPreplayed{false}; // Internal helpers ---------------------------------------------------- void refillBag(); diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 2cb608e..1c22150 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -473,6 +473,9 @@ void MenuState::handleEvent(const SDL_Event& e) { // Start challenge run at level 1 if (ctx.game) { ctx.game->setMode(GameMode::Challenge); + if (ctx.skipNextLevelUpJingle) { + *ctx.skipNextLevelUpJingle = true; + } ctx.game->startChallengeRun(1); } triggerPlay(); diff --git a/src/states/State.h b/src/states/State.h index 1a66418..ebbc25b 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -67,6 +67,7 @@ struct StateContext { int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default) bool* gameplayCountdownActive = nullptr; // True if start-of-game countdown is running bool* menuPlayCountdownArmed = nullptr; // True if we are transitioning to play and countdown is pending + bool* skipNextLevelUpJingle = nullptr; // Allows states to silence initial level-up SFX // Challenge clear FX (slow block-by-block explosion before next level) bool* challengeClearFxActive = nullptr; double* challengeClearFxElapsedMs = nullptr;