#include "MenuState.h" #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" #include #include #include #include #include #include // Use dynamic logical dimensions from GlobalState instead of hardcoded values // This allows the UI to adapt when the window is resized or goes fullscreen // Shared flags and resources are provided via StateContext `ctx`. // Removed fragile extern declarations and use `ctx.showLevelPopup`, `ctx.showSettingsPopup`, // `ctx.musicEnabled` and `ctx.hoveredButton` instead to avoid globals. // Menu helper wrappers are declared in a shared header implemented in main.cpp #include "../audio/MenuWrappers.h" #include "../utils/ImagePathResolver.h" #include "../graphics/renderers/UIRenderer.h" #include "../graphics/renderers/GameRenderer.h" #include MenuState::MenuState(StateContext& ctx) : State(ctx) {} void MenuState::onEnter() { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called"); if (ctx.showExitConfirmPopup) { *ctx.showExitConfirmPopup = false; } if (ctx.exitPopupSelectedButton) { *ctx.exitPopupSelectedButton = 1; } } 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; } // Clean up icon textures if (playIcon) { SDL_DestroyTexture(playIcon); playIcon = nullptr; } if (levelIcon) { SDL_DestroyTexture(levelIcon); levelIcon = nullptr; } if (optionsIcon) { SDL_DestroyTexture(optionsIcon); optionsIcon = nullptr; } if (exitIcon) { SDL_DestroyTexture(exitIcon); exitIcon = nullptr; } } 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; } }; auto getExitSelection = [&]() -> int { return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1; }; auto isExitPromptVisible = [&]() -> bool { return ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup; }; auto setExitPrompt = [&](bool visible) { if (ctx.showExitConfirmPopup) { *ctx.showExitConfirmPopup = visible; } }; if (isExitPromptVisible()) { switch (e.key.scancode) { case SDL_SCANCODE_LEFT: case SDL_SCANCODE_UP: setExitSelection(0); return; case SDL_SCANCODE_RIGHT: case SDL_SCANCODE_DOWN: setExitSelection(1); return; case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: case SDL_SCANCODE_SPACE: if (getExitSelection() == 0) { setExitPrompt(false); if (ctx.requestQuit) { ctx.requestQuit(); } else { SDL_Event quit{}; quit.type = SDL_EVENT_QUIT; SDL_PushEvent(&quit); } } else { setExitPrompt(false); } return; case SDL_SCANCODE_ESCAPE: setExitPrompt(false); setExitSelection(1); return; default: return; } } // 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: { const int total = 4; selectedButton = (selectedButton + total - 1) % total; break; } case SDL_SCANCODE_RIGHT: case SDL_SCANCODE_DOWN: { const int total = 4; selectedButton = (selectedButton + 1) % total; break; } case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: case SDL_SCANCODE_SPACE: if (!ctx.stateManager) { break; } switch (selectedButton) { case 0: triggerPlay(); break; case 1: // 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: // 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: setExitPrompt(true); setExitSelection(1); break; } 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; default: break; } } } 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) { // Use fixed logical dimensions to match main.cpp and ensure consistent layout // This prevents the UI from floating apart on wide/tall screens const float LOGICAL_W = 1200.f; const float LOGICAL_H = 1000.f; // Trace entry to persistent log for debugging abrupt exit/crash during render { FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render entry\n"); fclose(f); } } // Compute content offsets (same approach as main.cpp for proper centering) float winW = (float)logicalVP.w; float winH = (float)logicalVP.h; float contentOffsetX = 0.0f; 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"); if (f) { fprintf(f, "MenuState::render ctx.mainScreenTex=%llu (w=%d h=%d)\n", (unsigned long long)(uintptr_t)ctx.mainScreenTex, ctx.mainScreenW, ctx.mainScreenH); fclose(f); } } FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; // 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; useFont->measure(title, 1.8f, tW, tH); float titleX = (LOGICAL_W - (float)tW) * 0.5f + contentOffsetX; useFont->draw(renderer, titleX, topPlayersY, title, 1.8f, SDL_Color{255, 220, 0, 255}); } float scoresStartY = topPlayersY + 70.0f; static const std::vector EMPTY_SCORES; const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES; size_t maxDisplay = std::min(hs.size(), size_t(12)); if (useFont) { float cx = LOGICAL_W * 0.5f + contentOffsetX; float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 }; useFont->draw(renderer, colX[0], scoresStartY - 28, "RANK", 1.1f, SDL_Color{200,200,220,255}); useFont->draw(renderer, colX[1], scoresStartY - 28, "PLAYER", 1.1f, SDL_Color{200,200,220,255}); useFont->draw(renderer, colX[2], scoresStartY - 28, "SCORE", 1.1f, SDL_Color{200,200,220,255}); useFont->draw(renderer, colX[3], scoresStartY - 28, "LINES", 1.1f, SDL_Color{200,200,220,255}); useFont->draw(renderer, colX[4], scoresStartY - 28, "LEVEL", 1.1f, SDL_Color{200,200,220,255}); 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 - panelDelta; float wave = std::sin((float)GlobalState::instance().logoAnimCounter * 0.006f + i * 0.25f) * 6.0f; float y = baseY + wave; char rankStr[8]; std::snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1); useFont->draw(renderer, colX[0], y, rankStr, 1.0f, SDL_Color{220, 220, 230, 255}); useFont->draw(renderer, colX[1], y, hs[i].name, 1.0f, SDL_Color{220, 220, 230, 255}); char scoreStr[16]; std::snprintf(scoreStr, sizeof(scoreStr), "%d", hs[i].score); useFont->draw(renderer, colX[2], y, scoreStr, 1.0f, SDL_Color{220, 220, 230, 255}); char linesStr[8]; std::snprintf(linesStr, sizeof(linesStr), "%d", hs[i].lines); useFont->draw(renderer, colX[3], y, linesStr, 1.0f, SDL_Color{220, 220, 230, 255}); char levelStr[8]; std::snprintf(levelStr, sizeof(levelStr), "%d", hs[i].level); useFont->draw(renderer, colX[4], y, levelStr, 1.0f, SDL_Color{220, 220, 230, 255}); char timeStr[16]; int mins = int(hs[i].timeSec) / 60; int secs = int(hs[i].timeSec) % 60; std::snprintf(timeStr, sizeof(timeStr), "%d:%02d", mins, secs); useFont->draw(renderer, colX[5], y, timeStr, 1.0f, SDL_Color{220, 220, 230, 255}); } } // The main_screen overlay is drawn by main.cpp as the background // We don't need to draw it again here as a logo // Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test) // Use the contentW calculated at the top with content offsets float contentW = LOGICAL_W * logicalScale; bool isSmall = (contentW < 700.0f); // Adjust button dimensions to match the background button graphics float btnW = 200.0f; // Fixed width to match background buttons float btnH = 70.0f; // Fixed height to match background buttons float btnX = LOGICAL_W * 0.5f + contentOffsetX; // Adjust vertical position to align with background buttons float btnY = LOGICAL_H * 0.865f + contentOffsetY; if (ctx.pixelFont) { { FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render drawing buttons; pixelFont=%llu\n", (unsigned long long)(uintptr_t)ctx.pixelFont); fclose(f); } } 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" } }; // Icon array (nullptr if icon not loaded) std::array icons = { playIcon, levelIcon, optionsIcon, exitIcon }; // Fixed spacing to match background button positions float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f; // Draw each button individually so each can have its own coordinates 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]); } // 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]); } // 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]); } // 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]); } } } if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) { GameRenderer::renderExitPopup( renderer, ctx.pixelFont, winW, winH, logicalScale, ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1 ); } // Popups (settings only - level popup is now a separate state) if (ctx.showSettingsPopup && *ctx.showSettingsPopup) { bool musicOn = ctx.musicEnabled ? *ctx.musicEnabled : true; 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); } } }