From b531bbc798ebcf9c19beb0bffdfe654e57d9d868 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sat, 6 Dec 2025 14:08:24 +0100 Subject: [PATCH] fix --- src/states/MenuState.cpp | 233 +++++++++++++++++++++++++++++++++------ src/states/MenuState.h | 21 ++++ 2 files changed, 219 insertions(+), 35 deletions(-) diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 5e0625c..76d9ec0 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -85,9 +85,15 @@ void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale, if (i == 3) extra = -44.0f; cxCenter = btnX + offset + extra; } + // Apply group alpha and transient flash to button colors + double aMul = std::clamp(buttonGroupAlpha + buttonFlash * buttonFlashAmount, 0.0, 1.0); + SDL_Color bgCol = buttons[i].bg; + SDL_Color bdCol = buttons[i].border; + bgCol.a = static_cast(std::round(aMul * static_cast(bgCol.a))); + bdCol.a = static_cast(std::round(aMul * static_cast(bdCol.a))); UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, buttons[i].label, false, selectedButton == i, - buttons[i].bg, buttons[i].border, true, icons[i]); + bgCol, bdCol, true, icons[i]); } } @@ -131,36 +137,49 @@ void MenuState::handleEvent(const SDL_Event& e) { } }; - if (isExitPromptVisible()) { + // Inline exit HUD handling (replaces the old modal popup) + if (exitPanelVisible && !exitPanelAnimating) { switch (e.key.scancode) { case SDL_SCANCODE_LEFT: - case SDL_SCANCODE_UP: - setExitSelection(0); + // Move selection to YES + exitSelectedButton = 0; + if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton; return; case SDL_SCANCODE_RIGHT: - case SDL_SCANCODE_DOWN: - setExitSelection(1); + // Move selection to NO + exitSelectedButton = 1; + if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton; return; case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: case SDL_SCANCODE_SPACE: - if (getExitSelection() == 0) { - setExitPrompt(false); + if (exitSelectedButton == 0) { + // Confirm quit if (ctx.requestQuit) { ctx.requestQuit(); } else { - SDL_Event quit{}; - quit.type = SDL_EVENT_QUIT; - SDL_PushEvent(&quit); + SDL_Event quit{}; quit.type = SDL_EVENT_QUIT; SDL_PushEvent(&quit); } } else { - setExitPrompt(false); + // Close HUD + exitPanelAnimating = true; exitDirection = -1; + if (ctx.showExitConfirmPopup) *ctx.showExitConfirmPopup = false; } return; case SDL_SCANCODE_ESCAPE: - setExitPrompt(false); - setExitSelection(1); + // Close HUD + exitPanelAnimating = true; exitDirection = -1; + if (ctx.showExitConfirmPopup) *ctx.showExitConfirmPopup = false; return; + case SDL_SCANCODE_PAGEDOWN: + case SDL_SCANCODE_DOWN: { + // scroll content down + exitScroll += 40.0; return; + } + case SDL_SCANCODE_PAGEUP: + case SDL_SCANCODE_UP: { + exitScroll -= 40.0; if (exitScroll < 0.0) exitScroll = 0.0; return; + } default: return; } @@ -168,15 +187,17 @@ void MenuState::handleEvent(const SDL_Event& e) { // If the inline options HUD is visible and not animating, capture navigation if (optionsVisible && !optionsAnimating) { + // Options now has more rows; use OPTIONS_ROW_COUNT + constexpr int OPTIONS_ROW_COUNT = 8; switch (e.key.scancode) { case SDL_SCANCODE_UP: { - optionsSelectedRow = (optionsSelectedRow + 5 - 1) % 5; + optionsSelectedRow = (optionsSelectedRow + OPTIONS_ROW_COUNT - 1) % OPTIONS_ROW_COUNT; return; } case SDL_SCANCODE_DOWN: { - optionsSelectedRow = (optionsSelectedRow + 1) % 5; + optionsSelectedRow = (optionsSelectedRow + 1) % OPTIONS_ROW_COUNT; return; } case SDL_SCANCODE_LEFT: @@ -220,6 +241,25 @@ void MenuState::handleEvent(const SDL_Event& e) { return; } case 4: { + // PULSE ENABLE + buttonPulseEnabled = !buttonPulseEnabled; + return; + } + case 5: { + // PULSE SPEED (Enter toggles to default) + if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) { + buttonPulseSpeed = 1.0; + } + return; + } + case 6: { + // PULSE MIN ALPHA (Enter resets) + if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) { + buttonPulseMinAlpha = 0.6; + } + return; + } + case 7: { // RETURN TO MENU -> hide panel optionsAnimating = true; optionsDirection = -1; @@ -234,7 +274,8 @@ void MenuState::handleEvent(const SDL_Event& e) { // If inline level HUD visible and not animating, capture navigation if (levelPanelVisible && !levelPanelAnimating) { - int c = levelSelected < 0 ? 0 : levelSelected; + // Start navigation from tentative hover if present, otherwise from committed selection + int c = (levelHovered >= 0) ? levelHovered : (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; @@ -243,18 +284,22 @@ void MenuState::handleEvent(const SDL_Event& e) { case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: case SDL_SCANCODE_SPACE: + // Confirm tentative selection levelSelected = c; if (ctx.startLevelSelection) *ctx.startLevelSelection = levelSelected; // close HUD levelPanelAnimating = true; levelDirection = -1; + // clear hover candidate + levelHovered = -1; return; case SDL_SCANCODE_ESCAPE: levelPanelAnimating = true; levelDirection = -1; + levelHovered = -1; return; default: break; } - levelSelected = c; - if (ctx.startLevelSelection) *ctx.startLevelSelection = levelSelected; + // Move tentative cursor (don't commit to startLevelSelection yet) + levelHovered = c; // Consume the event so main menu navigation does not also run return; } @@ -265,6 +310,8 @@ void MenuState::handleEvent(const SDL_Event& e) { { const int total = 4; selectedButton = (selectedButton + total - 1) % total; + // brief bright flash on navigation + buttonFlash = 1.0; break; } case SDL_SCANCODE_RIGHT: @@ -272,6 +319,8 @@ void MenuState::handleEvent(const SDL_Event& e) { { const int total = 4; selectedButton = (selectedButton + 1) % total; + // brief bright flash on navigation + buttonFlash = 1.0; break; } case SDL_SCANCODE_RETURN: @@ -289,6 +338,8 @@ void MenuState::handleEvent(const SDL_Event& e) { if (!levelPanelVisible && !levelPanelAnimating) { levelPanelAnimating = true; levelDirection = 1; // show + // initialize tentative cursor to current selected level + levelHovered = levelSelected < 0 ? 0 : levelSelected; } else if (levelPanelVisible && !levelPanelAnimating) { levelPanelAnimating = true; levelDirection = -1; // hide @@ -305,8 +356,14 @@ void MenuState::handleEvent(const SDL_Event& e) { } break; case 3: - setExitPrompt(true); - setExitSelection(1); + // Show the inline exit HUD + if (!exitPanelVisible && !exitPanelAnimating) { + exitPanelAnimating = true; + exitDirection = 1; + exitSelectedButton = 1; + if (ctx.showExitConfirmPopup) *ctx.showExitConfirmPopup = true; + if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton; + } break; } break; @@ -317,8 +374,14 @@ void MenuState::handleEvent(const SDL_Event& e) { optionsDirection = -1; return; } - setExitPrompt(true); - setExitSelection(1); + // Show inline exit HUD on ESC + if (!exitPanelVisible && !exitPanelAnimating) { + exitPanelAnimating = true; + exitDirection = 1; + exitSelectedButton = 1; + if (ctx.showExitConfirmPopup) *ctx.showExitConfirmPopup = true; + if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton; + } break; default: break; @@ -359,6 +422,21 @@ void MenuState::update(double frameMs) { } } + // Advance exit panel animation if active + if (exitPanelAnimating) { + double delta = (frameMs / exitTransitionDurationMs) * static_cast(exitDirection); + exitTransition += delta; + if (exitTransition >= 1.0) { + exitTransition = 1.0; + exitPanelVisible = true; + exitPanelAnimating = false; + } else if (exitTransition <= 0.0) { + exitTransition = 0.0; + exitPanelVisible = false; + exitPanelAnimating = 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 @@ -381,7 +459,7 @@ void MenuState::update(double frameMs) { float cellW = (area.w - (cols - 1) * gapX) / cols; float cellH = (area.h - (rows - 1) * gapY) / rows; - int targetIdx = std::clamp(levelSelected, 0, 19); + int targetIdx = std::clamp((levelHovered >= 0 ? levelHovered : 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; @@ -399,6 +477,47 @@ void MenuState::update(double frameMs) { levelHighlightY += (targetY - levelHighlightY) * alpha; } } + + // Update button group pulsing animation + if (buttonPulseEnabled) { + buttonPulseTime += frameMs; + double t = (buttonPulseTime * 0.001) * buttonPulseSpeed; // seconds * speed + double s = 0.0; + switch (buttonPulseEasing) { + case 0: // sin + s = (std::sin(t * 2.0 * 3.14159265358979323846) * 0.5) + 0.5; + break; + case 1: // triangle + { + double ft = t - std::floor(t); + s = (ft < 0.5) ? (ft * 2.0) : (2.0 - ft * 2.0); + break; + } + case 2: // exponential ease-in-out (normalized) + { + double ft = t - std::floor(t); + if (ft < 0.5) { + s = 0.5 * std::pow(2.0, 20.0 * ft - 10.0); + } else { + s = 1.0 - 0.5 * std::pow(2.0, -20.0 * ft + 10.0); + } + // Clamp to 0..1 in case of numeric issues + if (s < 0.0) s = 0.0; if (s > 1.0) s = 1.0; + break; + } + default: + s = (std::sin(t * 2.0 * 3.14159265358979323846) * 0.5) + 0.5; + } + buttonGroupAlpha = buttonPulseMinAlpha + s * (buttonPulseMaxAlpha - buttonPulseMinAlpha); + } else { + buttonGroupAlpha = 1.0; + } + + // Update flash decay + if (buttonFlash > 0.0) { + buttonFlash -= frameMs * buttonFlashDecay; + if (buttonFlash < 0.0) buttonFlash = 0.0; + } } void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { @@ -439,8 +558,8 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi // 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)); + // Compute eased transition and delta to shift highscores when either options, level, or exit HUD is shown. + float combinedTransition = static_cast(std::max(std::max(optionsTransition, levelTransition), exitTransition)); float eased = combinedTransition * combinedTransition * (3.0f - 2.0f * combinedTransition); // cubic smoothstep float panelDelta = eased * moveAmount; @@ -617,15 +736,59 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi } } - if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) { - GameRenderer::renderExitPopup( - renderer, - ctx.pixelFont, - winW, - winH, - logicalScale, - ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1 - ); + // Inline exit HUD (no opaque background) - slides into the highscores area + if (exitTransition > 0.0) { + float easedE = static_cast(exitTransition); + easedE = easedE * easedE * (3.0f - 2.0f * easedE); + const float panelW = 520.0f; + const float panelH = 360.0f; + float panelBaseX = (LOGICAL_W - panelW) * 0.5f + contentOffsetX; + float panelBaseY = (LOGICAL_H - panelH) * 0.5f + contentOffsetY - (LOGICAL_H * 0.10f); + float slideAmount = LOGICAL_H * 0.42f; + float panelY = panelBaseY + (1.0f - easedE) * slideAmount; + + FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; + if (titleFont) titleFont->draw(renderer, panelBaseX + 12.0f, panelY + 6.0f, "EXIT GAME?", 1.6f, SDL_Color{255,200,80,220}); + + SDL_FRect area{ panelBaseX + 12.0f, panelY + 56.0f, panelW - 24.0f, panelH - 120.0f }; + // Sample long message (scrollable) + // Paragraph-style lines for a nicer exit confirmation message + std::vector lines = { + "Quit now to return to your desktop. Your current session will end.", + "Press YES to quit immediately, or NO to return to the menu and continue playing.", + "Adjust audio, controls and other settings anytime from the Options menu.", + "Thanks for playing SPACETRIS — we hope to see you again!" + }; + + // Draw scrollable text (no background box; increased line spacing) + FontAtlas* f = ctx.font ? ctx.font : ctx.pixelFont; + float y = area.y - (float)exitScroll; + const float lineSpacing = 34.0f; // increased spacing for readability + if (f) { + for (size_t i = 0; i < lines.size(); ++i) { + f->draw(renderer, area.x + 6.0f, y + i * lineSpacing, lines[i], 1.0f, SDL_Color{200,220,240,220}); + } + } + + // Draw buttons at bottom of panel + float btnW2 = 160.0f, btnH2 = 48.0f; + float bx = panelBaseX + (panelW - (btnW2 * 2.0f + 12.0f)) * 0.5f; + float by = panelY + panelH - 56.0f; + // YES button + SDL_Color yesBg{220,80,60, 200}; SDL_Color yesBorder{160,40,40,200}; + SDL_Color noBg{60,140,200,200}; SDL_Color noBorder{30,90,160,200}; + // Apply pulse alpha to buttons + double aMul = std::clamp(buttonGroupAlpha + buttonFlash * buttonFlashAmount, 0.0, 1.0); + yesBg.a = static_cast(std::round(aMul * static_cast(yesBg.a))); + yesBorder.a = static_cast(std::round(aMul * static_cast(yesBorder.a))); + noBg.a = static_cast(std::round(aMul * static_cast(noBg.a))); + noBorder.a = static_cast(std::round(aMul * static_cast(noBorder.a))); + + UIRenderer::drawButton(renderer, ctx.pixelFont, bx + btnW2*0.5f, by, btnW2, btnH2, "YES", false, exitSelectedButton == 0, yesBg, yesBorder, true, nullptr); + UIRenderer::drawButton(renderer, ctx.pixelFont, bx + btnW2*1.5f + 12.0f, by, btnW2, btnH2, "NO", false, exitSelectedButton == 1, noBg, noBorder, true, nullptr); + + // Ensure ctx mirrors selection for any other code + if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton; } // Popups (settings only - level popup is now a separate state) diff --git a/src/states/MenuState.h b/src/states/MenuState.h index 8b7bee0..0f5f5c1 100644 --- a/src/states/MenuState.h +++ b/src/states/MenuState.h @@ -53,4 +53,25 @@ private: 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}; + // Button group pulsing/fade parameters (applies to all four main buttons) + double buttonGroupAlpha = 1.0; // current computed alpha (0..1) + double buttonPulseTime = 0.0; // accumulator in ms + bool buttonPulseEnabled = true; // enable/disable pulsing + double buttonPulseSpeed = 1.0; // multiplier for pulse frequency + double buttonPulseMinAlpha = 0.60; // minimum alpha during pulse + double buttonPulseMaxAlpha = 1.00; // maximum alpha during pulse + // Pulse easing mode: 0=sin,1=triangle,2=exponential + int buttonPulseEasing = 1; + // Short bright flash when navigating buttons + double buttonFlash = 0.0; // transient flash amount (0..1) + double buttonFlashAmount = 0.45; // how much flash adds to group alpha + double buttonFlashDecay = 0.0025; // linear decay per ms + // Exit confirmation HUD state (inline HUD like Options/Level) + bool exitPanelVisible = false; + bool exitPanelAnimating = false; + double exitTransition = 0.0; // 0..1 + double exitTransitionDurationMs = 360.0; + int exitDirection = 1; // 1 show, -1 hide + int exitSelectedButton = 0; // 0 = YES (quit), 1 = NO (cancel) + double exitScroll = 0.0; // vertical scroll offset for content };