diff --git a/src/graphics/GameRenderer.cpp b/src/graphics/GameRenderer.cpp index f37a617..1211107 100644 --- a/src/graphics/GameRenderer.cpp +++ b/src/graphics/GameRenderer.cpp @@ -123,8 +123,11 @@ void GameRenderer::renderPlayingState( float logicalScale, float winW, float winH, - bool showExitConfirmPopup + bool showExitConfirmPopup, + int exitPopupSelectedButton, + bool suppressPauseVisuals ) { + (void)exitPopupSelectedButton; if (!game || !pixelFont) return; // Calculate actual content area (centered within the window) @@ -236,8 +239,10 @@ void GameRenderer::renderPlayingState( } } + bool allowActivePieceRender = !game->isPaused() || suppressPauseVisuals; + // Draw ghost piece (where current piece will land) - if (!game->isPaused()) { + if (allowActivePieceRender) { Game::Piece ghostPiece = game->current(); // Find landing position while (true) { @@ -270,7 +275,7 @@ void GameRenderer::renderPlayingState( } // Draw the falling piece - if (!game->isPaused()) { + if (allowActivePieceRender) { drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false); } @@ -412,8 +417,8 @@ void GameRenderer::renderPlayingState( drawSmallPiece(renderer, blocksTex, static_cast(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); } - // Pause overlay - if (game->isPaused() && !showExitConfirmPopup) { + // Pause overlay (suppressed when requested, e.g., countdown) + if (!suppressPauseVisuals && game->isPaused() && !showExitConfirmPopup) { SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); SDL_FRect pauseOverlay{0, 0, logicalW, logicalH}; SDL_RenderFillRect(renderer, &pauseOverlay); diff --git a/src/graphics/GameRenderer.h b/src/graphics/GameRenderer.h index 15e5192..4e7767e 100644 --- a/src/graphics/GameRenderer.h +++ b/src/graphics/GameRenderer.h @@ -26,7 +26,9 @@ public: float logicalScale, float winW, float winH, - bool showExitConfirmPopup + bool showExitConfirmPopup, + int exitPopupSelectedButton = 1, + bool suppressPauseVisuals = false ); private: diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 6abc259..69b80e9 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -124,7 +124,8 @@ void GameRenderer::renderPlayingState( float winW, float winH, bool showExitConfirmPopup, - int exitPopupSelectedButton + int exitPopupSelectedButton, + bool suppressPauseVisuals ) { if (!game || !pixelFont) return; @@ -237,8 +238,10 @@ void GameRenderer::renderPlayingState( } } + bool allowActivePieceRender = !game->isPaused() || suppressPauseVisuals; + // Draw ghost piece (where current piece will land) - if (!game->isPaused()) { + if (allowActivePieceRender) { Game::Piece ghostPiece = game->current(); // Find landing position while (true) { @@ -271,7 +274,7 @@ void GameRenderer::renderPlayingState( } // Draw the falling piece - if (!game->isPaused()) { + if (allowActivePieceRender) { drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false); } @@ -413,8 +416,8 @@ void GameRenderer::renderPlayingState( drawSmallPiece(renderer, blocksTex, static_cast(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); } - // Pause overlay - if (game->isPaused() && !showExitConfirmPopup) { + // Pause overlay (skip when visuals are suppressed, e.g., countdown) + if (!suppressPauseVisuals && game->isPaused() && !showExitConfirmPopup) { SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); SDL_FRect pauseOverlay{0, 0, logicalW, logicalH}; SDL_RenderFillRect(renderer, &pauseOverlay); diff --git a/src/graphics/renderers/GameRenderer.h b/src/graphics/renderers/GameRenderer.h index 6b72bb3..6ad6fee 100644 --- a/src/graphics/renderers/GameRenderer.h +++ b/src/graphics/renderers/GameRenderer.h @@ -27,7 +27,8 @@ public: float winW, float winH, bool showExitConfirmPopup, - int exitPopupSelectedButton = 1 // 0=YES, 1=NO + int exitPopupSelectedButton = 1, // 0=YES, 1=NO + bool suppressPauseVisuals = false ); private: diff --git a/src/main.cpp b/src/main.cpp index f773d13..ebb3069 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -626,6 +626,19 @@ int main(int, char **) bool musicLoaded = false; int currentTrackLoading = 0; int totalTracks = 0; // Will be set dynamically based on actual files + + enum class MenuFadePhase { None, FadeOut, FadeIn }; + MenuFadePhase menuFadePhase = MenuFadePhase::None; + double menuFadeClockMs = 0.0; + float menuFadeAlpha = 0.0f; + const double MENU_PLAY_FADE_DURATION_MS = 450.0; + AppState menuFadeTarget = AppState::Menu; + bool menuPlayCountdownArmed = false; + bool gameplayCountdownActive = false; + double gameplayCountdownElapsed = 0.0; + int gameplayCountdownIndex = 0; + const double GAMEPLAY_COUNTDOWN_STEP_MS = 600.0; + const std::array GAMEPLAY_COUNTDOWN_LABELS = { "3", "2", "1", "START" }; // Instantiate state manager StateManager stateMgr(state); @@ -666,6 +679,29 @@ int main(int, char **) running = false; }; + auto startMenuPlayTransition = [&]() { + if (!ctx.stateManager) { + return; + } + if (state != AppState::Menu) { + state = AppState::Playing; + ctx.stateManager->setState(state); + return; + } + if (menuFadePhase != MenuFadePhase::None) { + return; + } + menuFadePhase = MenuFadePhase::FadeOut; + menuFadeClockMs = 0.0; + menuFadeAlpha = 0.0f; + menuFadeTarget = AppState::Playing; + menuPlayCountdownArmed = true; + gameplayCountdownActive = false; + gameplayCountdownIndex = 0; + gameplayCountdownElapsed = 0.0; + }; + ctx.startPlayTransition = startMenuPlayTransition; + // Instantiate state objects auto loadingState = std::make_unique(ctx); auto menuState = std::make_unique(ctx); @@ -822,9 +858,7 @@ int main(int, char **) }; if (pointInRect(buttonRects[0])) { - game.reset(startLevelSelection); - state = AppState::Playing; - stateMgr.setState(state); + startMenuPlayTransition(); } else if (pointInRect(buttonRects[1])) { state = AppState::LevelSelector; stateMgr.setState(state); @@ -1180,6 +1214,63 @@ int main(int, char **) break; } + if (menuFadePhase == MenuFadePhase::FadeOut) { + menuFadeClockMs += frameMs; + menuFadeAlpha = std::min(1.0f, float(menuFadeClockMs / MENU_PLAY_FADE_DURATION_MS)); + if (menuFadeClockMs >= MENU_PLAY_FADE_DURATION_MS) { + if (menuFadeTarget == AppState::Playing) { + state = menuFadeTarget; + stateMgr.setState(state); + menuPlayCountdownArmed = true; + gameplayCountdownActive = false; + gameplayCountdownIndex = 0; + gameplayCountdownElapsed = 0.0; + game.setPaused(true); + } + menuFadePhase = MenuFadePhase::FadeIn; + menuFadeClockMs = MENU_PLAY_FADE_DURATION_MS; + menuFadeAlpha = 1.0f; + } + } else if (menuFadePhase == MenuFadePhase::FadeIn) { + menuFadeClockMs -= frameMs; + menuFadeAlpha = std::max(0.0f, float(menuFadeClockMs / MENU_PLAY_FADE_DURATION_MS)); + if (menuFadeClockMs <= 0.0) { + menuFadePhase = MenuFadePhase::None; + menuFadeClockMs = 0.0; + menuFadeAlpha = 0.0f; + } + } + + if (menuFadePhase == MenuFadePhase::None && menuPlayCountdownArmed && !gameplayCountdownActive && state == AppState::Playing) { + gameplayCountdownActive = true; + menuPlayCountdownArmed = false; + gameplayCountdownElapsed = 0.0; + gameplayCountdownIndex = 0; + game.setPaused(true); + } + + if (gameplayCountdownActive && state == AppState::Playing) { + gameplayCountdownElapsed += frameMs; + if (gameplayCountdownElapsed >= GAMEPLAY_COUNTDOWN_STEP_MS) { + gameplayCountdownElapsed -= GAMEPLAY_COUNTDOWN_STEP_MS; + ++gameplayCountdownIndex; + if (gameplayCountdownIndex >= static_cast(GAMEPLAY_COUNTDOWN_LABELS.size())) { + gameplayCountdownActive = false; + gameplayCountdownElapsed = 0.0; + gameplayCountdownIndex = 0; + game.setPaused(false); + } + } + } + + if (state != AppState::Playing && gameplayCountdownActive) { + gameplayCountdownActive = false; + menuPlayCountdownArmed = false; + gameplayCountdownElapsed = 0.0; + gameplayCountdownIndex = 0; + game.setPaused(false); + } + // --- Render --- SDL_SetRenderViewport(renderer, nullptr); SDL_SetRenderDrawColor(renderer, 12, 12, 16, 255); @@ -1387,7 +1478,8 @@ int main(int, char **) (float)winW, (float)winH, showExitConfirmPopup, - exitPopupSelectedButton + exitPopupSelectedButton, + (gameplayCountdownActive || menuPlayCountdownArmed) ); break; case AppState::GameOver: @@ -1514,6 +1606,44 @@ int main(int, char **) break; } + if (menuFadeAlpha > 0.0f) { + SDL_SetRenderViewport(renderer, nullptr); + SDL_SetRenderScale(renderer, 1.f, 1.f); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + Uint8 alpha = Uint8(std::clamp(menuFadeAlpha, 0.0f, 1.0f) * 255.0f); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, alpha); + SDL_FRect fadeRect{0.f, 0.f, (float)winW, (float)winH}; + SDL_RenderFillRect(renderer, &fadeRect); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); + SDL_SetRenderViewport(renderer, &logicalVP); + SDL_SetRenderScale(renderer, logicalScale, logicalScale); + } + + if (gameplayCountdownActive && state == AppState::Playing) { + SDL_SetRenderViewport(renderer, nullptr); + SDL_SetRenderScale(renderer, 1.f, 1.f); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160); + SDL_FRect dimRect{0.f, 0.f, (float)winW, (float)winH}; + SDL_RenderFillRect(renderer, &dimRect); + + SDL_SetRenderViewport(renderer, &logicalVP); + SDL_SetRenderScale(renderer, logicalScale, logicalScale); + + int cappedIndex = std::min(gameplayCountdownIndex, static_cast(GAMEPLAY_COUNTDOWN_LABELS.size()) - 1); + const char* label = GAMEPLAY_COUNTDOWN_LABELS[cappedIndex]; + bool isFinalCue = (cappedIndex == static_cast(GAMEPLAY_COUNTDOWN_LABELS.size()) - 1); + float textScale = isFinalCue ? 2.6f : 3.6f; + int textW = 0, textH = 0; + pixelFont.measure(label, textScale, textW, textH); + float textX = (LOGICAL_W - static_cast(textW)) * 0.5f; + float textY = (LOGICAL_H - static_cast(textH)) * 0.5f; + SDL_Color textColor = isFinalCue ? SDL_Color{255, 230, 90, 255} : SDL_Color{255, 255, 255, 255}; + pixelFont.draw(renderer, textX, textY, label, textScale, textColor); + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); + } + SDL_RenderPresent(renderer); SDL_SetRenderScale(renderer, 1.f, 1.f); } diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 457ce10..39657bd 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -40,6 +40,14 @@ void MenuState::onExit() { void MenuState::handleEvent(const SDL_Event& e) { // Keyboard navigation for menu buttons if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { + auto triggerPlay = [&]() { + if (ctx.startPlayTransition) { + ctx.startPlayTransition(); + } else if (ctx.stateManager) { + ctx.stateManager->setState(AppState::Playing); + } + }; + auto setExitSelection = [&](int value) { if (ctx.exitPopupSelectedButton) { *ctx.exitPopupSelectedButton = value; @@ -115,7 +123,7 @@ void MenuState::handleEvent(const SDL_Event& e) { } switch (selectedButton) { case 0: - ctx.stateManager->setState(AppState::Playing); + triggerPlay(); break; case 1: ctx.stateManager->setState(AppState::LevelSelector); diff --git a/src/states/State.h b/src/states/State.h index d6146b2..a439c70 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -55,6 +55,7 @@ struct StateContext { std::function applyFullscreen; // Allows states to request fullscreen changes std::function queryFullscreen; // Optional callback if fullscreenFlag is not reliable std::function requestQuit; // Allows menu/option states to close the app gracefully + std::function startPlayTransition; // Optional fade hook when transitioning from menu to gameplay // Pointer to the application's StateManager so states can request transitions StateManager* stateManager = nullptr; };