diff --git a/CMakeLists.txt b/CMakeLists.txt index af802f5..8eba3f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ set(TETRIS_SOURCES src/gameplay/effects/LineEffect.cpp src/audio/SoundEffect.cpp src/ui/MenuLayout.cpp + src/ui/BottomMenu.cpp src/app/BackgroundManager.cpp src/app/Fireworks.cpp src/app/AssetLoader.cpp diff --git a/src/main.cpp b/src/main.cpp index 3598b13..483b0bf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,6 +45,7 @@ #include "core/Config.h" #include "core/Settings.h" #include "ui/MenuLayout.h" +#include "ui/BottomMenu.h" // Debug logging removed: no-op in this build (previously LOG_DEBUG) @@ -981,24 +982,30 @@ int main(int, char **) showSettingsPopup = false; } else { ui::MenuLayoutParams params{ LOGICAL_W, LOGICAL_H, winW, winH, logicalScale }; - auto buttonRects = ui::computeMenuButtonRects(params); - auto pointInRect = [&](const SDL_FRect& r) { - return lx >= r.x && lx <= r.x + r.w && ly >= r.y && ly <= r.y + r.h; - }; + auto menuInput = ui::handleBottomMenuInput(params, e, lx, ly, hoveredButton, true); + hoveredButton = menuInput.hoveredIndex; - if (pointInRect(buttonRects[0])) { - startMenuPlayTransition(); - } else if (pointInRect(buttonRects[1])) { - requestStateFade(AppState::LevelSelector); - } else if (pointInRect(buttonRects[2])) { - requestStateFade(AppState::Options); - } else if (pointInRect(buttonRects[3])) { - // HELP - show inline help HUD in the MenuState - if (menuState) menuState->showHelpPanel(true); - } else if (pointInRect(buttonRects[4])) { - showExitConfirmPopup = true; - exitPopupSelectedButton = 1; + if (menuInput.activated) { + switch (*menuInput.activated) { + case ui::BottomMenuItem::Play: + startMenuPlayTransition(); + break; + case ui::BottomMenuItem::Level: + requestStateFade(AppState::LevelSelector); + break; + case ui::BottomMenuItem::Options: + requestStateFade(AppState::Options); + break; + case ui::BottomMenuItem::Help: + // HELP - show inline help HUD in the MenuState + if (menuState) menuState->showHelpPanel(true); + break; + case ui::BottomMenuItem::Exit: + showExitConfirmPopup = true; + exitPopupSelectedButton = 1; + break; + } } // Settings button (gear icon area - top right) @@ -1099,7 +1106,8 @@ int main(int, char **) float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; ui::MenuLayoutParams params{ LOGICAL_W, LOGICAL_H, winW, winH, logicalScale }; - hoveredButton = ui::hitTestMenuButtons(params, lx, ly); + auto menuInput = ui::handleBottomMenuInput(params, e, lx, ly, hoveredButton, true); + hoveredButton = menuInput.hoveredIndex; } } } diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 9cece0a..484d584 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -26,6 +26,7 @@ #include "../graphics/renderers/UIRenderer.h" #include "../graphics/renderers/GameRenderer.h" #include "../ui/MenuLayout.h" +#include "../ui/BottomMenu.h" #include // Frosted tint helper: draw a safe, inexpensive frosted overlay for the panel area. @@ -145,69 +146,16 @@ void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale, logicalVP.h, logicalScale }; - auto rects = ui::computeMenuButtonRects(params); - // 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); + ui::BottomMenu menu = ui::buildBottomMenu(params, 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,200,60,255}, SDL_Color{150,150,40,255}, "HELP" }, - MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" } - }; - - std::array icons = { playIcon, levelIcon, optionsIcon, helpIcon, exitIcon }; - - - // Draw PLAY as a real glowing button, and the four bottom items as HUD buttons. - for (int i = 0; i < 5; ++i) { - const SDL_FRect& r = rects[i]; - float cxCenter = r.x + r.w * 0.5f; - float cyCenter = r.y + r.h * 0.5f; - float btnW = r.w; - float btnH = r.h; - - const bool isHovered = (ctx.hoveredButton && *ctx.hoveredButton == i); - const bool isSelected = (selectedButton == i); - - double aMul = std::clamp(buttonGroupAlpha + buttonFlash * buttonFlashAmount, 0.0, 1.0); - - if (i == 0) { - SDL_Color bgCol{ 18, 22, 28, static_cast(std::round(180.0 * aMul)) }; - SDL_Color bdCol{ 255, 200, 70, static_cast(std::round(220.0 * aMul)) }; - UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, - buttons[i].label, isHovered, isSelected, - bgCol, bdCol, false, nullptr); - } else { - SDL_Color bgCol{ 20, 30, 42, static_cast(std::round(160.0 * aMul)) }; - SDL_Color bdCol{ 120, 220, 255, static_cast(std::round(200.0 * aMul)) }; - UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH, - buttons[i].label, isHovered, isSelected, - bgCol, bdCol, true, nullptr); - } - } - - // Draw small '+' separators between the bottom HUD buttons (matches the reference vibe). - { - SDL_BlendMode prevBlend = SDL_BLENDMODE_NONE; - SDL_GetRenderDrawBlendMode(renderer, &prevBlend); - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - SDL_SetRenderDrawColor(renderer, 120, 220, 255, 180); - - float y = rects[1].y + rects[1].h * 0.5f; - for (int i = 1; i < 4; ++i) { - float x = (rects[i].x + rects[i].w + rects[i + 1].x) * 0.5f; - SDL_RenderLine(renderer, x - 4.0f, y, x + 4.0f, y); - SDL_RenderLine(renderer, x, y - 4.0f, x, y + 4.0f); - } - - SDL_SetRenderDrawBlendMode(renderer, prevBlend); - } + const int hovered = (ctx.hoveredButton ? *ctx.hoveredButton : -1); + const double baseAlpha = 1.0; + // Pulse is encoded as a signed delta so PLAY can dim/brighten while focused. + const double pulseDelta = (buttonPulseAlpha - 1.0); + const double flashDelta = buttonFlash * buttonFlashAmount; + ui::renderBottomMenu(renderer, ctx.pixelFont, menu, hovered, selectedButton, baseAlpha, pulseDelta + flashDelta); } void MenuState::onExit() { @@ -226,6 +174,11 @@ 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) { + // When the player uses the keyboard, don't let an old mouse hover keep focus on a button. + if (ctx.hoveredButton) { + *ctx.hoveredButton = -1; + } + auto triggerPlay = [&]() { if (ctx.startPlayTransition) { ctx.startPlayTransition(); @@ -622,7 +575,7 @@ void MenuState::update(double frameMs) { } } - // Update button group pulsing animation + // Update pulsing animation (used for PLAY emphasis) if (buttonPulseEnabled) { buttonPulseTime += frameMs; double t = (buttonPulseTime * 0.001) * buttonPulseSpeed; // seconds * speed @@ -652,11 +605,14 @@ void MenuState::update(double frameMs) { default: s = (std::sin(t * 2.0 * 3.14159265358979323846) * 0.5) + 0.5; } - buttonGroupAlpha = buttonPulseMinAlpha + s * (buttonPulseMaxAlpha - buttonPulseMinAlpha); + buttonPulseAlpha = buttonPulseMinAlpha + s * (buttonPulseMaxAlpha - buttonPulseMinAlpha); } else { - buttonGroupAlpha = 1.0; + buttonPulseAlpha = 1.0; } + // Keep the base group alpha stable; pulsing is applied selectively in the renderer. + buttonGroupAlpha = 1.0; + // Update flash decay if (buttonFlash > 0.0) { buttonFlash -= frameMs * buttonFlashDecay; diff --git a/src/states/MenuState.h b/src/states/MenuState.h index 32243d9..9b2a766 100644 --- a/src/states/MenuState.h +++ b/src/states/MenuState.h @@ -56,8 +56,9 @@ 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) + // Button pulsing/fade parameters (used for PLAY emphasis) + double buttonGroupAlpha = 1.0; // base alpha for the whole group (kept stable) + double buttonPulseAlpha = 1.0; // pulsing alpha (0..1), applied to PLAY only double buttonPulseTime = 0.0; // accumulator in ms bool buttonPulseEnabled = true; // enable/disable pulsing double buttonPulseSpeed = 1.0; // multiplier for pulse frequency diff --git a/src/ui/BottomMenu.cpp b/src/ui/BottomMenu.cpp new file mode 100644 index 0000000..27b4059 --- /dev/null +++ b/src/ui/BottomMenu.cpp @@ -0,0 +1,126 @@ +#include "ui/BottomMenu.h" + +#include +#include +#include + +#include "graphics/renderers/UIRenderer.h" +#include "graphics/Font.h" + +namespace ui { + +static bool pointInRect(const SDL_FRect& r, float x, float y) { + return x >= r.x && x <= (r.x + r.w) && y >= r.y && y <= (r.y + r.h); +} + +BottomMenu buildBottomMenu(const MenuLayoutParams& params, int startLevel) { + BottomMenu menu{}; + + auto rects = computeMenuButtonRects(params); + + char levelBtnText[32]; + std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel); + + menu.buttons[0] = Button{ BottomMenuItem::Play, rects[0], "PLAY", false }; + menu.buttons[1] = Button{ BottomMenuItem::Level, rects[1], levelBtnText, true }; + menu.buttons[2] = Button{ BottomMenuItem::Options, rects[2], "OPTIONS", true }; + menu.buttons[3] = Button{ BottomMenuItem::Help, rects[3], "HELP", true }; + menu.buttons[4] = Button{ BottomMenuItem::Exit, rects[4], "EXIT", true }; + + return menu; +} + +void renderBottomMenu(SDL_Renderer* renderer, + FontAtlas* font, + const BottomMenu& menu, + int hoveredIndex, + int selectedIndex, + double baseAlphaMul, + double flashAddMul) { + if (!renderer || !font) return; + + const double baseMul = std::clamp(baseAlphaMul, 0.0, 1.0); + const double flashMul = flashAddMul; + + const int focusIndex = (hoveredIndex != -1) ? hoveredIndex : selectedIndex; + + for (int i = 0; i < MENU_BTN_COUNT; ++i) { + const Button& b = menu.buttons[i]; + const SDL_FRect& r = b.rect; + + float cx = r.x + r.w * 0.5f; + float cy = r.y + r.h * 0.5f; + + bool isHovered = (hoveredIndex == i); + bool isSelected = (selectedIndex == i); + + // Requested behavior: flash only the PLAY button, and only when it's the active/focused item. + const bool playIsActive = (i == 0) && (focusIndex == 0); + const double aMul = std::clamp(baseMul + (playIsActive ? flashMul : 0.0), 0.0, 1.0); + + if (!b.textOnly) { + SDL_Color bgCol{ 18, 22, 28, static_cast(std::round(180.0 * aMul)) }; + SDL_Color bdCol{ 255, 200, 70, static_cast(std::round(220.0 * aMul)) }; + UIRenderer::drawButton(renderer, font, cx, cy, r.w, r.h, + b.label, isHovered, isSelected, + bgCol, bdCol, false, nullptr); + } else { + SDL_Color bgCol{ 20, 30, 42, static_cast(std::round(160.0 * aMul)) }; + SDL_Color bdCol{ 120, 220, 255, static_cast(std::round(200.0 * aMul)) }; + UIRenderer::drawButton(renderer, font, cx, cy, r.w, r.h, + b.label, isHovered, isSelected, + bgCol, bdCol, true, nullptr); + } + } + + // '+' separators between the bottom HUD buttons (indices 1..4) + { + SDL_BlendMode prevBlend = SDL_BLENDMODE_NONE; + SDL_GetRenderDrawBlendMode(renderer, &prevBlend); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, 120, 220, 255, static_cast(std::round(180.0 * baseMul))); + + float y = menu.buttons[1].rect.y + menu.buttons[1].rect.h * 0.5f; + for (int i = 1; i < 4; ++i) { + float x = (menu.buttons[i].rect.x + menu.buttons[i].rect.w + menu.buttons[i + 1].rect.x) * 0.5f; + SDL_RenderLine(renderer, x - 4.0f, y, x + 4.0f, y); + SDL_RenderLine(renderer, x, y - 4.0f, x, y + 4.0f); + } + + SDL_SetRenderDrawBlendMode(renderer, prevBlend); + } +} + +BottomMenuInputResult handleBottomMenuInput(const MenuLayoutParams& params, + const SDL_Event& e, + float x, + float y, + int prevHoveredIndex, + bool inputEnabled) { + BottomMenuInputResult result{}; + result.hoveredIndex = prevHoveredIndex; + + if (!inputEnabled) { + return result; + } + + if (e.type == SDL_EVENT_MOUSE_MOTION) { + result.hoveredIndex = hitTestMenuButtons(params, x, y); + return result; + } + + if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) { + auto rects = computeMenuButtonRects(params); + for (int i = 0; i < MENU_BTN_COUNT; ++i) { + if (pointInRect(rects[i], x, y)) { + result.activated = static_cast(i); + result.hoveredIndex = i; + break; + } + } + } + + return result; +} + +} // namespace ui diff --git a/src/ui/BottomMenu.h b/src/ui/BottomMenu.h new file mode 100644 index 0000000..7627232 --- /dev/null +++ b/src/ui/BottomMenu.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include + +#include "ui/MenuLayout.h" +#include "ui/UIConstants.h" + +struct FontAtlas; + +namespace ui { + +enum class BottomMenuItem : int { + Play = 0, + Level = 1, + Options = 2, + Help = 3, + Exit = 4, +}; + +struct Button { + BottomMenuItem item{}; + SDL_FRect rect{}; + std::string label; + bool textOnly = true; +}; + +struct BottomMenu { + std::array buttons{}; +}; + +BottomMenu buildBottomMenu(const MenuLayoutParams& params, int startLevel); + +// Draws the cockpit HUD menu (PLAY + 4 bottom items) using existing UIRenderer primitives. +// hoveredIndex: -1..4 +// selectedIndex: 0..4 (keyboard selection) +// alphaMul: 0..1 (overall group alpha) +void renderBottomMenu(SDL_Renderer* renderer, + FontAtlas* font, + const BottomMenu& menu, + int hoveredIndex, + int selectedIndex, + double baseAlphaMul, + double flashAddMul); + +struct BottomMenuInputResult { + int hoveredIndex = -1; + std::optional activated; +}; + +// Interprets mouse motion/button input for the bottom menu. +// Expects x/y in the same logical coordinate space used by MenuLayout (the current main loop already provides this). +BottomMenuInputResult handleBottomMenuInput(const MenuLayoutParams& params, + const SDL_Event& e, + float x, + float y, + int prevHoveredIndex, + bool inputEnabled); + +} // namespace ui diff --git a/src/ui/MenuLayout.cpp b/src/ui/MenuLayout.cpp index 15c9a5c..84dae86 100644 --- a/src/ui/MenuLayout.cpp +++ b/src/ui/MenuLayout.cpp @@ -38,7 +38,9 @@ std::array computeMenuButtonRects(const MenuLayoutParams& p) { float centerX = LOGICAL_W * 0.5f + contentOffsetX; float bottomY = LOGICAL_H + contentOffsetY - marginBottom; float smallCY = bottomY - smallH * 0.5f; - float playCY = smallCY - smallH * 0.5f - 16.0f - playH * 0.5f; + // Extra breathing room between PLAY and the bottom row (requested). + const float rowGap = 34.0f; + float playCY = smallCY - smallH * 0.5f - rowGap - playH * 0.5f; std::array rects{}; rects[0] = SDL_FRect{ centerX - playW * 0.5f, playCY - playH * 0.5f, playW, playH };