diff --git a/assets/images/main_screen_004.png b/assets/images/main_screen_004.png index f884159..4feabfe 100644 Binary files a/assets/images/main_screen_004.png and b/assets/images/main_screen_004.png differ diff --git a/src/main.cpp b/src/main.cpp index 57091f2..7118974 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -624,7 +624,7 @@ int main(int, char **) // Load the new main screen overlay that sits above the background but below buttons int mainScreenW = 0; int mainScreenH = 0; - SDL_Texture* mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen_003.png", &mainScreenW, &mainScreenH); + SDL_Texture* mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen_004.png", &mainScreenW, &mainScreenH); if (mainScreenTex) { SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded main_screen overlay %dx%d (tex=%p)", mainScreenW, mainScreenH, (void*)mainScreenTex); @@ -1574,28 +1574,8 @@ int main(int, char **) } else if (state == AppState::Menu) { // Space flyover backdrop for the main screen spaceWarp.draw(renderer, 1.0f); - - if (mainScreenTex) { - float texW = mainScreenW > 0 ? static_cast(mainScreenW) : 0.0f; - float texH = mainScreenH > 0 ? static_cast(mainScreenH) : 0.0f; - if (texW <= 0.0f || texH <= 0.0f) { - if (!SDL_GetTextureSize(mainScreenTex, &texW, &texH)) { - texW = texH = 0.0f; - } - } - if (texW > 0.0f && texH > 0.0f) { - const float drawH = static_cast(winH); - const float scale = drawH / texH; - const float drawW = texW * scale; - SDL_FRect dst{ - (winW - drawW) * 0.5f, - 0.0f, - drawW, - drawH - }; - SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst); - } - } + // `mainScreenTex` is rendered as a top layer just before presenting + // so we don't draw it here. Keep the space warp background only. } else if (state == AppState::LevelSelector || state == AppState::Options) { if (backgroundTex) { SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH }; @@ -1927,6 +1907,43 @@ int main(int, char **) HelpOverlay::Render(renderer, pixelFont, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY); } + // Top-layer overlay: render `mainScreenTex` above all other layers when in Menu + if (state == AppState::Menu && mainScreenTex) { + SDL_SetRenderViewport(renderer, nullptr); + SDL_SetRenderScale(renderer, 1.f, 1.f); + float texW = mainScreenW > 0 ? static_cast(mainScreenW) : 0.0f; + float texH = mainScreenH > 0 ? static_cast(mainScreenH) : 0.0f; + if (texW <= 0.0f || texH <= 0.0f) { + float iwf = 0.0f, ihf = 0.0f; + if (SDL_GetTextureSize(mainScreenTex, &iwf, &ihf) != 0) { + iwf = ihf = 0.0f; + } + texW = iwf; + texH = ihf; + } + if (texW > 0.0f && texH > 0.0f) { + const float drawH = static_cast(winH); + const float scale = drawH / texH; + const float drawW = texW * scale; + SDL_FRect dst{ + (winW - drawW) * 0.5f, + 0.0f, + drawW, + drawH + }; + SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND); + SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst); + } + // Restore logical viewport/scale and draw the main PLAY button above the overlay + SDL_SetRenderViewport(renderer, &logicalVP); + SDL_SetRenderScale(renderer, logicalScale, logicalScale); + if (menuState) { + menuState->drawMainButtonNormally = false; // ensure it isn't double-drawn + menuState->renderMainButtonTop(renderer, logicalScale, logicalVP); + menuState->drawMainButtonNormally = true; + } + } + SDL_RenderPresent(renderer); SDL_SetRenderScale(renderer, 1.f, 1.f); } diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index f00ce92..5e0625c 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -2,6 +2,7 @@ #include "persistence/Scores.h" #include "graphics/Font.h" #include "../core/GlobalState.h" +#include "../core/Settings.h" #include "../core/state/StateManager.h" #include "../audio/Audio.h" #include "../audio/SoundEffect.h" @@ -37,6 +38,59 @@ void MenuState::onEnter() { } } +void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { + const float LOGICAL_W = 1200.f; + const float LOGICAL_H = 1000.f; + float contentOffsetX = 0.0f; + float contentOffsetY = 0.0f; + UIRenderer::computeContentOffsets((float)logicalVP.w, (float)logicalVP.h, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY); + + float contentW = LOGICAL_W * logicalScale; + bool isSmall = (contentW < 700.0f); + float btnW = 200.0f; + float btnH = 70.0f; + float btnX = LOGICAL_W * 0.5f + contentOffsetX; + float btnY = LOGICAL_H * 0.865f + contentOffsetY; + + // Compose same button definition used in render() + char levelBtnText[32]; + int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0; + std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel); + + struct MenuButtonDef { SDL_Color bg; SDL_Color border; std::string label; }; + std::array buttons = { + MenuButtonDef{ SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}, "PLAY" }, + MenuButtonDef{ SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}, levelBtnText }, + MenuButtonDef{ SDL_Color{130,80,210,255}, SDL_Color{90,40,170,255}, "OPTIONS" }, + MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" } + }; + + std::array icons = { playIcon, levelIcon, optionsIcon, exitIcon }; + + float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f; + + // Draw all four buttons on top + for (int i = 0; i < 4; ++i) { + float cxCenter = 0.0f; + float cyCenter = btnY; + if (ctx.menuButtonsExplicit) { + cxCenter = ctx.menuButtonCX[i] + contentOffsetX; + cyCenter = ctx.menuButtonCY[i] + contentOffsetY; + } else { + float offset = (static_cast(i) - 1.5f) * spacing; + // Apply small per-button offsets that match the original placements + float extra = 0.0f; + if (i == 0) extra = 15.0f; + if (i == 2) extra = -24.0f; + if (i == 3) extra = -44.0f; + cxCenter = btnX + offset + extra; + } + UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, + buttons[i].label, false, selectedButton == i, + buttons[i].bg, buttons[i].border, true, icons[i]); + } +} + void MenuState::onExit() { if (ctx.showExitConfirmPopup) { *ctx.showExitConfirmPopup = false; @@ -112,6 +166,99 @@ void MenuState::handleEvent(const SDL_Event& e) { } } + // If the inline options HUD is visible and not animating, capture navigation + if (optionsVisible && !optionsAnimating) { + switch (e.key.scancode) { + case SDL_SCANCODE_UP: + { + optionsSelectedRow = (optionsSelectedRow + 5 - 1) % 5; + return; + } + case SDL_SCANCODE_DOWN: + { + optionsSelectedRow = (optionsSelectedRow + 1) % 5; + return; + } + case SDL_SCANCODE_LEFT: + case SDL_SCANCODE_RIGHT: + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + case SDL_SCANCODE_SPACE: + { + // Perform toggle/action for the selected option + switch (optionsSelectedRow) { + case 0: { + // FULLSCREEN + bool nextState = ! (ctx.fullscreenFlag ? *ctx.fullscreenFlag : Settings::instance().isFullscreen()); + if (ctx.fullscreenFlag) *ctx.fullscreenFlag = nextState; + if (ctx.applyFullscreen) ctx.applyFullscreen(nextState); + Settings::instance().setFullscreen(nextState); + Settings::instance().save(); + return; + } + case 1: { + // MUSIC + bool next = !Settings::instance().isMusicEnabled(); + Settings::instance().setMusicEnabled(next); + Settings::instance().save(); + if (ctx.musicEnabled) *ctx.musicEnabled = next; + return; + } + case 2: { + // SOUND FX + bool next = !SoundEffectManager::instance().isEnabled(); + SoundEffectManager::instance().setEnabled(next); + Settings::instance().setSoundEnabled(next); + Settings::instance().save(); + return; + } + case 3: { + // SMOOTH SCROLL + bool next = !Settings::instance().isSmoothScrollEnabled(); + Settings::instance().setSmoothScrollEnabled(next); + Settings::instance().save(); + return; + } + case 4: { + // RETURN TO MENU -> hide panel + optionsAnimating = true; + optionsDirection = -1; + return; + } + } + } + default: + break; + } + } + + // If inline level HUD visible and not animating, capture navigation + if (levelPanelVisible && !levelPanelAnimating) { + int c = levelSelected < 0 ? 0 : levelSelected; + switch (e.key.scancode) { + case SDL_SCANCODE_LEFT: if (c % 4 > 0) c--; break; + case SDL_SCANCODE_RIGHT: if (c % 4 < 3) c++; break; + case SDL_SCANCODE_UP: if (c / 4 > 0) c -= 4; break; + case SDL_SCANCODE_DOWN: if (c / 4 < 4) c += 4; break; + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + case SDL_SCANCODE_SPACE: + levelSelected = c; + if (ctx.startLevelSelection) *ctx.startLevelSelection = levelSelected; + // close HUD + levelPanelAnimating = true; levelDirection = -1; + return; + case SDL_SCANCODE_ESCAPE: + levelPanelAnimating = true; levelDirection = -1; + return; + default: break; + } + levelSelected = c; + if (ctx.startLevelSelection) *ctx.startLevelSelection = levelSelected; + // Consume the event so main menu navigation does not also run + return; + } + switch (e.key.scancode) { case SDL_SCANCODE_LEFT: case SDL_SCANCODE_UP: @@ -138,17 +285,23 @@ void MenuState::handleEvent(const SDL_Event& e) { triggerPlay(); break; case 1: - if (ctx.requestFadeTransition) { - ctx.requestFadeTransition(AppState::LevelSelector); - } else if (ctx.stateManager) { - ctx.stateManager->setState(AppState::LevelSelector); + // Toggle inline level selector HUD (show/hide) + if (!levelPanelVisible && !levelPanelAnimating) { + levelPanelAnimating = true; + levelDirection = 1; // show + } else if (levelPanelVisible && !levelPanelAnimating) { + levelPanelAnimating = true; + levelDirection = -1; // hide } break; case 2: - if (ctx.requestFadeTransition) { - ctx.requestFadeTransition(AppState::Options); - } else if (ctx.stateManager) { - ctx.stateManager->setState(AppState::Options); + // Toggle the options panel with an animated slide-in/out. + if (!optionsVisible && !optionsAnimating) { + optionsAnimating = true; + optionsDirection = 1; // show + } else if (optionsVisible && !optionsAnimating) { + optionsAnimating = true; + optionsDirection = -1; // hide } break; case 3: @@ -158,6 +311,12 @@ void MenuState::handleEvent(const SDL_Event& e) { } break; case SDL_SCANCODE_ESCAPE: + // If options panel is visible, hide it first. + if (optionsVisible && !optionsAnimating) { + optionsAnimating = true; + optionsDirection = -1; + return; + } setExitPrompt(true); setExitSelection(1); break; @@ -170,6 +329,76 @@ void MenuState::handleEvent(const SDL_Event& e) { void MenuState::update(double frameMs) { // Update logo animation counter GlobalState::instance().logoAnimCounter += frameMs; + // Advance options panel animation if active + if (optionsAnimating) { + double delta = (frameMs / optionsTransitionDurationMs) * static_cast(optionsDirection); + optionsTransition += delta; + if (optionsTransition >= 1.0) { + optionsTransition = 1.0; + optionsVisible = true; + optionsAnimating = false; + } else if (optionsTransition <= 0.0) { + optionsTransition = 0.0; + optionsVisible = false; + optionsAnimating = false; + } + } + + // Advance level panel animation if active + if (levelPanelAnimating) { + double delta = (frameMs / levelTransitionDurationMs) * static_cast(levelDirection); + levelTransition += delta; + if (levelTransition >= 1.0) { + levelTransition = 1.0; + levelPanelVisible = true; + levelPanelAnimating = false; + } else if (levelTransition <= 0.0) { + levelTransition = 0.0; + levelPanelVisible = false; + levelPanelAnimating = false; + } + } + + // Animate level selection highlight position toward the selected cell center + if (levelTransition > 0.0 && (lastLogicalScale > 0.0f)) { + // Recompute same grid geometry used in render to find target center + const float LOGICAL_W = 1200.f; + const float LOGICAL_H = 1000.f; + float winW = (float)lastLogicalVP.w; + float winH = (float)lastLogicalVP.h; + float contentOffsetX = 0.0f; + float contentOffsetY = 0.0f; + UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, lastLogicalScale, contentOffsetX, contentOffsetY); + + const float PW = std::min(520.0f, LOGICAL_W * 0.65f); + const float PH = std::min(360.0f, LOGICAL_H * 0.7f); + float panelBaseX = (LOGICAL_W - PW) * 0.5f + contentOffsetX; + float panelBaseY = (LOGICAL_H - PH) * 0.5f + contentOffsetY - (LOGICAL_H * 0.10f); + float marginX = 34.0f, marginY = 56.0f; + SDL_FRect area{ panelBaseX + marginX, panelBaseY + marginY, PW - 2.0f * marginX, PH - marginY - 28.0f }; + const int cols = 4, rows = 5; + const float gapX = 12.0f, gapY = 12.0f; + float cellW = (area.w - (cols - 1) * gapX) / cols; + float cellH = (area.h - (rows - 1) * gapY) / rows; + + int targetIdx = std::clamp(levelSelected, 0, 19); + int tr = targetIdx / cols, tc = targetIdx % cols; + double targetX = area.x + tc * (cellW + gapX) + cellW * 0.5f; + double targetY = area.y + tr * (cellH + gapY) + cellH * 0.5f; + + if (!levelHighlightInitialized) { + levelHighlightX = targetX; + levelHighlightY = targetY; + levelHighlightInitialized = true; + } else { + // Exponential smoothing: alpha = 1 - exp(-k * dt) + double k = levelHighlightSpeed; // user-tunable speed constant + double alpha = 1.0 - std::exp(-k * frameMs); + if (alpha < 1e-6) alpha = std::min(1.0, frameMs * 0.02); + levelHighlightX += (targetX - levelHighlightX) * alpha; + levelHighlightY += (targetY - levelHighlightY) * alpha; + } + } } void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { @@ -190,6 +419,10 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi float contentOffsetY = 0.0f; UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY); + // Cache logical viewport/scale for update() so it can compute HUD target positions + lastLogicalScale = logicalScale; + lastLogicalVP = logicalVP; + // Background is drawn by main (stretched to the full window) to avoid double-draw. { FILE* f = fopen("tetris_trace.log", "a"); @@ -203,7 +436,15 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi } FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; - float topPlayersY = LOGICAL_H * 0.24f + contentOffsetY; + // Slide-space amount for the options HUD (how much highscores move) + const float moveAmount = 420.0f; // increased so lower score rows slide further up + + // Compute eased transition and delta to shift highscores when either options or level HUD is shown. + float combinedTransition = static_cast(std::max(optionsTransition, levelTransition)); + float eased = combinedTransition * combinedTransition * (3.0f - 2.0f * combinedTransition); // cubic smoothstep + float panelDelta = eased * moveAmount; + + float topPlayersY = LOGICAL_H * 0.24f + contentOffsetY - panelDelta; if (useFont) { const std::string title = "TOP PLAYERS"; int tW = 0, tH = 0; @@ -228,7 +469,7 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi useFont->draw(renderer, colX[5], scoresStartY - 28, "TIME", 1.1f, SDL_Color{200,200,220,255}); for (size_t i = 0; i < maxDisplay; ++i) { - float baseY = scoresStartY + i * 25.0f; + float baseY = scoresStartY + i * 25.0f - panelDelta; float wave = std::sin((float)GlobalState::instance().logoAnimCounter * 0.006f + i * 0.25f) * 6.0f; float y = baseY + wave; @@ -305,72 +546,74 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f; // Draw each button individually so each can have its own coordinates - // Button 0 - PLAY - { - const int i = 0; - float cxCenter = 0.0f; - float cyCenter = btnY; - if (ctx.menuButtonsExplicit) { - cxCenter = ctx.menuButtonCX[i] + contentOffsetX; - cyCenter = ctx.menuButtonCY[i] + contentOffsetY; - } else { - float offset = (static_cast(i) - 1.5f) * spacing; - cxCenter = btnX + offset + 15.0f; + if (drawMainButtonNormally) { + // Button 0 - PLAY + { + const int i = 0; + float cxCenter = 0.0f; + float cyCenter = btnY; + if (ctx.menuButtonsExplicit) { + cxCenter = ctx.menuButtonCX[i] + contentOffsetX; + cyCenter = ctx.menuButtonCY[i] + contentOffsetY; + } else { + float offset = (static_cast(i) - 1.5f) * spacing; + cxCenter = btnX + offset + 15.0f; + } + UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, + buttons[i].label, false, selectedButton == i, + buttons[i].bg, buttons[i].border, true, icons[i]); } - UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, - buttons[i].label, false, selectedButton == i, - buttons[i].bg, buttons[i].border, true, icons[i]); - } - // Button 1 - LEVEL - { - const int i = 1; - float cxCenter = 0.0f; - float cyCenter = btnY; - if (ctx.menuButtonsExplicit) { - cxCenter = ctx.menuButtonCX[i] + contentOffsetX; - cyCenter = ctx.menuButtonCY[i] + contentOffsetY; - } else { - float offset = (static_cast(i) - 1.5f) * spacing; - cxCenter = btnX + offset; + // Button 1 - LEVEL + { + const int i = 1; + float cxCenter = 0.0f; + float cyCenter = btnY; + if (ctx.menuButtonsExplicit) { + cxCenter = ctx.menuButtonCX[i] + contentOffsetX; + cyCenter = ctx.menuButtonCY[i] + contentOffsetY; + } else { + float offset = (static_cast(i) - 1.5f) * spacing; + cxCenter = btnX + offset; + } + UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, + buttons[i].label, false, selectedButton == i, + buttons[i].bg, buttons[i].border, true, icons[i]); } - UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, - buttons[i].label, false, selectedButton == i, - buttons[i].bg, buttons[i].border, true, icons[i]); - } - // Button 2 - OPTIONS - { - const int i = 2; - float cxCenter = 0.0f; - float cyCenter = btnY; - if (ctx.menuButtonsExplicit) { - cxCenter = ctx.menuButtonCX[i] + contentOffsetX; - cyCenter = ctx.menuButtonCY[i] + contentOffsetY; - } else { - float offset = (static_cast(i) - 1.5f) * spacing; - cxCenter = btnX + offset - 24.0f; + // Button 2 - OPTIONS + { + const int i = 2; + float cxCenter = 0.0f; + float cyCenter = btnY; + if (ctx.menuButtonsExplicit) { + cxCenter = ctx.menuButtonCX[i] + contentOffsetX; + cyCenter = ctx.menuButtonCY[i] + contentOffsetY; + } else { + float offset = (static_cast(i) - 1.5f) * spacing; + cxCenter = btnX + offset - 24.0f; + } + UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, + buttons[i].label, false, selectedButton == i, + buttons[i].bg, buttons[i].border, true, icons[i]); } - UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, - buttons[i].label, false, selectedButton == i, - buttons[i].bg, buttons[i].border, true, icons[i]); - } - // Button 3 - EXIT - { - const int i = 3; - float cxCenter = 0.0f; - float cyCenter = btnY; - if (ctx.menuButtonsExplicit) { - cxCenter = ctx.menuButtonCX[i] + contentOffsetX; - cyCenter = ctx.menuButtonCY[i] + contentOffsetY; - } else { - float offset = (static_cast(i) - 1.5f) * spacing; - cxCenter = btnX + offset - 44.0f; + // Button 3 - EXIT + { + const int i = 3; + float cxCenter = 0.0f; + float cyCenter = btnY; + if (ctx.menuButtonsExplicit) { + cxCenter = ctx.menuButtonCX[i] + contentOffsetX; + cyCenter = ctx.menuButtonCY[i] + contentOffsetY; + } else { + float offset = (static_cast(i) - 1.5f) * spacing; + cxCenter = btnX + offset - 44.0f; + } + UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, + buttons[i].label, false, selectedButton == i, + buttons[i].bg, buttons[i].border, true, icons[i]); } - UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, - buttons[i].label, false, selectedButton == i, - buttons[i].bg, buttons[i].border, true, icons[i]); } } @@ -391,6 +634,157 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi bool soundOn = SoundEffectManager::instance().isEnabled(); UIRenderer::drawSettingsPopup(renderer, ctx.font, LOGICAL_W, LOGICAL_H, musicOn, soundOn); } + + // Draw animated options panel if in use (either animating or visible) + if (optionsTransition > 0.0) { + // HUD-style overlay: no opaque background; draw labels/values directly with separators + const float panelW = 520.0f; + const float panelH = 420.0f; + float panelBaseX = (LOGICAL_W - panelW) * 0.5f + contentOffsetX; + // Move the HUD higher by ~10% of logical height so it sits above center + float panelBaseY = (LOGICAL_H - panelH) * 0.5f + contentOffsetY - (LOGICAL_H * 0.10f); + float panelY = panelBaseY + (1.0f - eased) * moveAmount; + + // For options/settings we prefer the secondary (Exo2) font for longer descriptions. + FontAtlas* retroFont = ctx.font ? ctx.font : ctx.pixelFont; + + if (retroFont) { + retroFont->draw(renderer, panelBaseX + 12.0f, panelY + 6.0f, "OPTIONS", 1.8f, SDL_Color{120, 220, 255, 220}); + } + + SDL_FRect area{panelBaseX, panelY + 48.0f, panelW, panelH - 64.0f}; + constexpr int rowCount = 5; + const float rowHeight = 64.0f; + const float spacing = 8.0f; + + auto drawField = [&](int idx, float y, const std::string& label, const std::string& value) { + SDL_FRect row{area.x, y, area.w, rowHeight}; + + // Draw thin separator (1px high filled rect) so we avoid platform-specific line API differences + SDL_SetRenderDrawColor(renderer, 60, 120, 160, 120); + SDL_FRect sep{ row.x + 6.0f, row.y + row.h - 1.0f, row.w - 12.0f, 1.0f }; + SDL_RenderFillRect(renderer, &sep); + + // Highlight the selected row with a subtle outline + if (idx == optionsSelectedRow) { + SDL_SetRenderDrawColor(renderer, 80, 200, 255, 120); + SDL_RenderRect(renderer, &row); + } + + if (retroFont) { + SDL_Color labelColor = SDL_Color{170, 210, 220, 220}; + SDL_Color valueColor = SDL_Color{160, 240, 255, 240}; + if (!label.empty()) { + float labelScale = 1.0f; + int labelW = 0, labelH = 0; + retroFont->measure(label, labelScale, labelW, labelH); + float labelY = row.y + (row.h - static_cast(labelH)) * 0.5f; + retroFont->draw(renderer, row.x + 16.0f, labelY, label, labelScale, labelColor); + } + int valueW = 0, valueH = 0; + float valueScale = 1.4f; + retroFont->measure(value, valueScale, valueW, valueH); + float valX = row.x + row.w - static_cast(valueW) - 16.0f; + float valY = row.y + (row.h - valueH) * 0.5f; + retroFont->draw(renderer, valX, valY, value, valueScale, valueColor); + } + }; + + float rowY = area.y + spacing; + // FULLSCREEN + bool isFS = ctx.fullscreenFlag ? *ctx.fullscreenFlag : Settings::instance().isFullscreen(); + drawField(0, rowY, "FULLSCREEN", isFS ? "ON" : "OFF"); + rowY += rowHeight + spacing; + // MUSIC + bool musicOn = ctx.musicEnabled ? *ctx.musicEnabled : Settings::instance().isMusicEnabled(); + drawField(1, rowY, "MUSIC", musicOn ? "ON" : "OFF"); + rowY += rowHeight + spacing; + // SOUND FX + bool soundOn = SoundEffectManager::instance().isEnabled(); + drawField(2, rowY, "SOUND FX", soundOn ? "ON" : "OFF"); + rowY += rowHeight + spacing; + // SMOOTH SCROLL + bool smooth = Settings::instance().isSmoothScrollEnabled(); + drawField(3, rowY, "SMOOTH SCROLL", smooth ? "ON" : "OFF"); + rowY += rowHeight + spacing; + // RETURN TO MENU + drawField(4, rowY, "", "RETURN TO MENU"); + } + + // Draw inline level selector HUD (no background) if active + if (levelTransition > 0.0) { + float easedL = static_cast(levelTransition); + easedL = easedL * easedL * (3.0f - 2.0f * easedL); + const float PW = std::min(520.0f, LOGICAL_W * 0.65f); + const float PH = std::min(360.0f, LOGICAL_H * 0.7f); + float panelBaseX = (LOGICAL_W - PW) * 0.5f + contentOffsetX; + float panelBaseY = (LOGICAL_H - PH) * 0.5f + contentOffsetY - (LOGICAL_H * 0.10f); + float slideAmount = LOGICAL_H * 0.42f; + float panelY = panelBaseY + (1.0f - easedL) * slideAmount; + + // Header + FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; + if (titleFont) titleFont->draw(renderer, panelBaseX + PW * 0.5f - 140.0f, panelY + 6.0f, "SELECT STARTING LEVEL", 1.1f, SDL_Color{160,220,255,220}); + + // Grid area + float marginX = 34.0f, marginY = 56.0f; + SDL_FRect area{ panelBaseX + marginX, panelY + marginY, PW - 2.0f * marginX, PH - marginY - 28.0f }; + const int cols = 4, rows = 5; + const float gapX = 12.0f, gapY = 12.0f; + float cellW = (area.w - (cols - 1) * gapX) / cols; + float cellH = (area.h - (rows - 1) * gapY) / rows; + + for (int i = 0; i < 20; ++i) { + int r = i / cols, c = i % cols; + SDL_FRect rc{ area.x + c * (cellW + gapX), area.y + r * (cellH + gapY), cellW, cellH }; + bool hovered = (levelSelected == i) || (levelHovered == i); + bool selected = (ctx.startLevelSelection && *ctx.startLevelSelection == i); + SDL_Color fill = selected ? SDL_Color{255,140,40,160} : (hovered ? SDL_Color{60,80,100,120} : SDL_Color{30,40,60,110}); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, fill.r, fill.g, fill.b, fill.a); + SDL_RenderFillRect(renderer, &rc); + SDL_SetRenderDrawColor(renderer, 80,100,120,160); + SDL_RenderRect(renderer, &rc); + + // Draw level number + FontAtlas* f = ctx.font ? ctx.font : ctx.pixelFont; + if (f) { + char buf[8]; std::snprintf(buf, sizeof(buf), "%d", i); + int w=0,h=0; f->measure(buf, 1.6f, w, h); + f->draw(renderer, rc.x + (rc.w - w) * 0.5f, rc.y + (rc.h - h) * 0.5f, buf, 1.6f, SDL_Color{220,230,240,255}); + } + } + + // Draw animated highlight (interpolated) on top of cells + if (levelHighlightInitialized) { + float hx = (float)levelHighlightX; + float hy = (float)levelHighlightY; + float hw = cellW + 6.0f; + float hh = cellH + 6.0f; + // Draw multi-layer glow: outer faint, mid, inner bright + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + // Outer glow + SDL_SetRenderDrawColor(renderer, levelHighlightColor.r, levelHighlightColor.g, levelHighlightColor.b, (Uint8)(levelHighlightGlowAlpha * 80)); + SDL_FRect outer{ hx - (hw * 0.5f + 8.0f), hy - (hh * 0.5f + 8.0f), hw + 16.0f, hh + 16.0f }; + SDL_RenderRect(renderer, &outer); + // Mid glow + SDL_SetRenderDrawColor(renderer, levelHighlightColor.r, levelHighlightColor.g, levelHighlightColor.b, (Uint8)(levelHighlightGlowAlpha * 140)); + SDL_FRect mid{ hx - (hw * 0.5f + 4.0f), hy - (hh * 0.5f + 4.0f), hw + 8.0f, hh + 8.0f }; + SDL_RenderRect(renderer, &mid); + // Inner outline + SDL_SetRenderDrawColor(renderer, levelHighlightColor.r, levelHighlightColor.g, levelHighlightColor.b, levelHighlightColor.a); + SDL_FRect inner{ hx - hw * 0.5f, hy - hh * 0.5f, hw, hh }; + // Draw multiple rects to simulate thickness + for (int t = 0; t < levelHighlightThickness; ++t) { + SDL_FRect r{ inner.x - t, inner.y - t, inner.w + t * 2.0f, inner.h + t * 2.0f }; + SDL_RenderRect(renderer, &r); + } + } + + // Instructions + FontAtlas* foot = ctx.font ? ctx.font : ctx.pixelFont; + if (foot) foot->draw(renderer, panelBaseX + PW*0.5f - 160.0f, panelY + PH + 40.0f, "ARROWS = NAV • ENTER = SELECT • ESC = CANCEL", 0.9f, SDL_Color{160,180,200,200}); + } // Trace exit { FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render exit\n"); fclose(f); } diff --git a/src/states/MenuState.h b/src/states/MenuState.h index 344b10d..8b7bee0 100644 --- a/src/states/MenuState.h +++ b/src/states/MenuState.h @@ -10,6 +10,11 @@ public: void handleEvent(const SDL_Event& e) override; void update(double frameMs) override; void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override; + // When false, the main PLAY button is not drawn by `render()` and can be + // rendered separately with `renderMainButtonTop` (useful for layer ordering). + bool drawMainButtonNormally = true; + // Draw only the main PLAY button on top of other layers (expects logical coords). + void renderMainButtonTop(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP); private: int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = EXIT @@ -19,4 +24,33 @@ private: SDL_Texture* levelIcon = nullptr; SDL_Texture* optionsIcon = nullptr; SDL_Texture* exitIcon = nullptr; + + // Options panel animation state + bool optionsVisible = false; + bool optionsAnimating = false; + double optionsTransition = 0.0; // 0..1 + double optionsTransitionDurationMs = 400.0; + int optionsDirection = 1; // 1 show, -1 hide + // Which row in the inline options panel is currently selected (0..4) + int optionsSelectedRow = 0; + // Inline level selector HUD state + bool levelPanelVisible = false; + bool levelPanelAnimating = false; + double levelTransition = 0.0; // 0..1 + double levelTransitionDurationMs = 400.0; + int levelDirection = 1; // 1 show, -1 hide + int levelHovered = -1; // hovered cell + int levelSelected = 0; // current selected level + // Cache logical viewport/scale for input conversion when needed + float lastLogicalScale = 1.0f; + SDL_Rect lastLogicalVP{0,0,0,0}; + // Animated highlight position (world/logical coordinates) + double levelHighlightX = 0.0; + double levelHighlightY = 0.0; + bool levelHighlightInitialized = false; + // Highlight tuning parameters + double levelHighlightSpeed = 0.018; // smoothing constant - higher = snappier + double levelHighlightGlowAlpha = 0.70; // 0..1 base glow alpha + int levelHighlightThickness = 3; // inner outline thickness (px) + SDL_Color levelHighlightColor = SDL_Color{80, 200, 255, 200}; };