From 4e69ed97423b21fc7efe4f1deea423d0b9829f18 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sat, 22 Nov 2025 12:16:47 +0100 Subject: [PATCH] added buttons to main state --- CMakeLists.txt | 2 + src/core/application/ApplicationManager.cpp | 53 +++++ src/core/application/ApplicationManager.h | 4 + src/core/state/StateManager.cpp | 1 + src/core/state/StateManager.h | 1 + src/main.cpp | 88 ++++++-- src/main_dist.cpp | 89 ++++++-- src/states/MenuState.cpp | 184 +++++++++++++-- src/states/MenuState.h | 2 +- src/states/OptionsState.cpp | 238 ++++++++++++++++++++ src/states/OptionsState.h | 35 +++ src/states/State.h | 7 + 12 files changed, 644 insertions(+), 60 deletions(-) create mode 100644 src/states/OptionsState.cpp create mode 100644 src/states/OptionsState.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 472ae7a..000fcb5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ add_executable(tetris # State implementations (new) src/states/LoadingState.cpp src/states/MenuState.cpp + src/states/OptionsState.cpp src/states/LevelSelectorState.cpp src/states/PlayingState.cpp ) @@ -144,6 +145,7 @@ add_executable(tetris_refactored # State implementations src/states/LoadingState.cpp src/states/MenuState.cpp + src/states/OptionsState.cpp src/states/LevelSelectorState.cpp src/states/PlayingState.cpp ) diff --git a/src/core/application/ApplicationManager.cpp b/src/core/application/ApplicationManager.cpp index 400b1fe..d4a3120 100644 --- a/src/core/application/ApplicationManager.cpp +++ b/src/core/application/ApplicationManager.cpp @@ -12,6 +12,7 @@ #include "../../states/State.h" #include "../../states/LoadingState.h" #include "../../states/MenuState.h" +#include "../../states/OptionsState.h" #include "../../states/LevelSelectorState.h" #include "../../states/PlayingState.h" #include "../assets/AssetManager.h" @@ -285,6 +286,7 @@ bool ApplicationManager::initializeManagers() { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize RenderManager"); return false; } + m_isFullscreen = m_renderManager->isFullscreen(); // Create InputManager m_inputManager = std::make_unique(); @@ -330,6 +332,7 @@ bool ApplicationManager::initializeManagers() { if (m_renderManager) { bool fs = m_renderManager->isFullscreen(); m_renderManager->setFullscreen(!fs); + m_isFullscreen = m_renderManager->isFullscreen(); } // Don’t also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start") // Don't also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start") @@ -588,10 +591,30 @@ bool ApplicationManager::initializeGame() { m_stateContext.showSettingsPopup = &m_showSettingsPopup; m_stateContext.showExitConfirmPopup = &m_showExitConfirmPopup; m_stateContext.exitPopupSelectedButton = &m_exitPopupSelectedButton; + m_stateContext.playerName = &m_playerName; + m_stateContext.fullscreenFlag = &m_isFullscreen; + m_stateContext.applyFullscreen = [this](bool enable) { + if (m_renderManager) { + m_renderManager->setFullscreen(enable); + m_isFullscreen = m_renderManager->isFullscreen(); + } else { + m_isFullscreen = enable; + } + }; + m_stateContext.queryFullscreen = [this]() -> bool { + if (m_renderManager) { + return m_renderManager->isFullscreen(); + } + return m_isFullscreen; + }; + m_stateContext.requestQuit = [this]() { + requestShutdown(); + }; // Create state instances m_loadingState = std::make_unique(m_stateContext); m_menuState = std::make_unique(m_stateContext); + m_optionsState = std::make_unique(m_stateContext); m_levelSelectorState = std::make_unique(m_stateContext); m_playingState = std::make_unique(m_stateContext); @@ -605,6 +628,10 @@ bool ApplicationManager::initializeGame() { m_stateManager->registerOnEnter(AppState::Menu, [this](){ if (m_menuState) m_menuState->onEnter(); }); m_stateManager->registerOnExit(AppState::Menu, [this](){ if (m_menuState) m_menuState->onExit(); }); + m_stateManager->registerEventHandler(AppState::Options, [this](const SDL_Event& e){ if (m_optionsState) m_optionsState->handleEvent(e); }); + m_stateManager->registerOnEnter(AppState::Options, [this](){ if (m_optionsState) m_optionsState->onEnter(); }); + m_stateManager->registerOnExit(AppState::Options, [this](){ if (m_optionsState) m_optionsState->onExit(); }); + m_stateManager->registerEventHandler(AppState::LevelSelector, [this](const SDL_Event& e){ if (m_levelSelectorState) m_levelSelectorState->handleEvent(e); }); m_stateManager->registerOnEnter(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onEnter(); }); m_stateManager->registerOnExit(AppState::LevelSelector, [this](){ if (m_levelSelectorState) m_levelSelectorState->onExit(); }); @@ -720,6 +747,32 @@ void ApplicationManager::setupStateHandlers() { SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f); }); + m_stateManager->registerRenderHandler(AppState::Options, + [this](RenderManager& renderer) { + renderer.clear(0, 0, 20, 255); + int winW = 0, winH = 0; + if (m_renderManager) m_renderManager->getWindowSize(winW, winH); + SDL_Texture* background = m_assetManager->getTexture("background"); + if (background && winW > 0 && winH > 0) { + SDL_FRect bgRect = { 0, 0, (float)winW, (float)winH }; + renderer.renderTexture(background, nullptr, &bgRect); + } + + SDL_Rect logicalVP = {0,0,0,0}; + float logicalScale = 1.0f; + if (m_renderManager) { + logicalVP = m_renderManager->getLogicalViewport(); + logicalScale = m_renderManager->getLogicalScale(); + } + SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP); + SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale); + if (m_optionsState) { + m_optionsState->render(renderer.getSDLRenderer(), logicalScale, logicalVP); + } + SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr); + SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f); + }); + // LevelSelector State render: draw background full-screen, then delegate to LevelSelectorState::render m_stateManager->registerRenderHandler(AppState::LevelSelector, [this](RenderManager& renderer) { diff --git a/src/core/application/ApplicationManager.h b/src/core/application/ApplicationManager.h index 0ed7992..386f31d 100644 --- a/src/core/application/ApplicationManager.h +++ b/src/core/application/ApplicationManager.h @@ -21,6 +21,7 @@ class LineEffect; // Forward declare state classes (top-level, defined under src/states) class LoadingState; class MenuState; +class OptionsState; class LevelSelectorState; class PlayingState; @@ -95,6 +96,8 @@ private: bool m_showSettingsPopup = false; bool m_showExitConfirmPopup = false; int m_exitPopupSelectedButton = 1; // 0 = YES, 1 = NO + bool m_isFullscreen = false; + std::string m_playerName = "PLAYER"; uint64_t m_loadStartTicks = 0; bool m_musicStarted = false; bool m_musicLoaded = false; @@ -120,6 +123,7 @@ private: // State objects (mirror main.cpp pattern) std::unique_ptr m_loadingState; std::unique_ptr m_menuState; + std::unique_ptr m_optionsState; std::unique_ptr m_levelSelectorState; std::unique_ptr m_playingState; // Application state diff --git a/src/core/state/StateManager.cpp b/src/core/state/StateManager.cpp index 6a08e50..d34a187 100644 --- a/src/core/state/StateManager.cpp +++ b/src/core/state/StateManager.cpp @@ -170,6 +170,7 @@ const char* StateManager::getStateName(AppState state) const { switch (state) { case AppState::Loading: return "Loading"; case AppState::Menu: return "Menu"; + case AppState::Options: return "Options"; case AppState::LevelSelector: return "LevelSelector"; case AppState::Playing: return "Playing"; case AppState::LevelSelect: return "LevelSelect"; diff --git a/src/core/state/StateManager.h b/src/core/state/StateManager.h index 307c6d6..7805475 100644 --- a/src/core/state/StateManager.h +++ b/src/core/state/StateManager.h @@ -13,6 +13,7 @@ class RenderManager; enum class AppState { Loading, Menu, + Options, LevelSelector, Playing, LevelSelect, diff --git a/src/main.cpp b/src/main.cpp index 7600ab5..5561d37 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,7 @@ #include "states/State.h" #include "states/LoadingState.h" #include "states/MenuState.h" +#include "states/OptionsState.h" #include "states/LevelSelectorState.h" #include "states/PlayingState.h" #include "audio/MenuWrappers.h" @@ -652,10 +653,23 @@ int main(int, char **) ctx.showSettingsPopup = &showSettingsPopup; ctx.showExitConfirmPopup = &showExitConfirmPopup; ctx.exitPopupSelectedButton = &exitPopupSelectedButton; + ctx.playerName = &playerName; + ctx.fullscreenFlag = &isFullscreen; + ctx.applyFullscreen = [window, &isFullscreen](bool enable) { + SDL_SetWindowFullscreen(window, enable ? SDL_WINDOW_FULLSCREEN : 0); + isFullscreen = enable; + }; + ctx.queryFullscreen = [window]() -> bool { + return (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0; + }; + ctx.requestQuit = [&running]() { + running = false; + }; // Instantiate state objects auto loadingState = std::make_unique(ctx); auto menuState = std::make_unique(ctx); + auto optionsState = std::make_unique(ctx); auto levelSelectorState = std::make_unique(ctx); auto playingState = std::make_unique(ctx); @@ -668,6 +682,10 @@ int main(int, char **) stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); }); stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); }); + stateMgr.registerHandler(AppState::Options, [&](const SDL_Event& e){ optionsState->handleEvent(e); }); + stateMgr.registerOnEnter(AppState::Options, [&](){ optionsState->onEnter(); }); + stateMgr.registerOnExit(AppState::Options, [&](){ optionsState->onExit(); }); + stateMgr.registerHandler(AppState::LevelSelector, [&](const SDL_Event& e){ levelSelectorState->handleEvent(e); }); stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); }); stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->onExit(); }); @@ -792,21 +810,30 @@ int main(int, char **) float btnCX = LOGICAL_W * 0.5f + contentOffsetX; const float btnYOffset = 40.0f; // must match MenuState offset float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; - SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; - SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; + float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; + std::array buttonRects{}; + for (int i = 0; i < 4; ++i) { + float center = btnCX + (static_cast(i) - 1.5f) * spacing; + buttonRects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH}; + } - if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) - { - // Reset the game first with the chosen start level so HUD and - // Playing state see the correct 0-based level immediately. + auto pointInRect = [&](const SDL_FRect& r) { + return lx >= r.x && lx <= r.x + r.w && ly >= r.y && ly <= r.y + r.h; + }; + + if (pointInRect(buttonRects[0])) { game.reset(startLevelSelection); state = AppState::Playing; stateMgr.setState(state); - } - else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) - { + } else if (pointInRect(buttonRects[1])) { state = AppState::LevelSelector; stateMgr.setState(state); + } else if (pointInRect(buttonRects[2])) { + state = AppState::Options; + stateMgr.setState(state); + } else if (pointInRect(buttonRects[3])) { + showExitConfirmPopup = true; + exitPopupSelectedButton = 1; } // Settings button (gear icon area - top right) @@ -865,6 +892,32 @@ int main(int, char **) game.setPaused(false); } } + else if (state == AppState::Menu && showExitConfirmPopup) { + float contentW = LOGICAL_W * logicalScale; + float contentH = LOGICAL_H * logicalScale; + float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; + float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; + float popupW = 420.0f; + float popupH = 230.0f; + float popupX = (LOGICAL_W - popupW) * 0.5f + contentOffsetX; + float popupY = (LOGICAL_H - popupH) * 0.5f + contentOffsetY; + float btnW = 140.0f; + float btnH = 50.0f; + float yesX = popupX + popupW * 0.3f - btnW / 2.0f; + float noX = popupX + popupW * 0.7f - btnW / 2.0f; + float btnY = popupY + popupH - btnH - 30.0f; + bool insidePopup = lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH; + if (insidePopup) { + if (lx >= yesX && lx <= yesX + btnW && ly >= btnY && ly <= btnY + btnH) { + showExitConfirmPopup = false; + running = false; + } else if (lx >= noX && lx <= noX + btnW && ly >= btnY && ly <= btnY + btnH) { + showExitConfirmPopup = false; + } + } else { + showExitConfirmPopup = false; + } + } } } else if (e.type == SDL_EVENT_MOUSE_MOTION) @@ -886,15 +939,16 @@ int main(int, char **) float btnCX = LOGICAL_W * 0.5f + contentOffsetX; const float btnYOffset = 40.0f; // must match MenuState offset float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; - SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; - SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; - - // Check menu button hovers (no level popup to handle anymore) + float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; hoveredButton = -1; - if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) - hoveredButton = 0; - else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) - hoveredButton = 1; + for (int i = 0; i < 4; ++i) { + float center = btnCX + (static_cast(i) - 1.5f) * spacing; + SDL_FRect rect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH}; + if (lx >= rect.x && lx <= rect.x + rect.w && ly >= rect.y && ly <= rect.y + rect.h) { + hoveredButton = i; + break; + } + } } } } diff --git a/src/main_dist.cpp b/src/main_dist.cpp index 9fad828..4771989 100644 --- a/src/main_dist.cpp +++ b/src/main_dist.cpp @@ -26,6 +26,7 @@ #include "states/State.h" #include "states/LoadingState.h" #include "states/MenuState.h" +#include "states/OptionsState.h" #include "states/LevelSelectorState.h" #include "states/PlayingState.h" #include "audio/MenuWrappers.h" @@ -279,6 +280,7 @@ static bool showExitConfirmPopup = false; static int exitPopupSelectedButton = 1; // 0 = YES, 1 = NO static bool musicEnabled = true; static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings +static std::string playerName = "PLAYER"; // ----------------------------------------------------------------------------- // Tetris Block Fireworks for intro animation (block particles) @@ -646,10 +648,23 @@ int main(int, char **) ctx.showSettingsPopup = &showSettingsPopup; ctx.showExitConfirmPopup = &showExitConfirmPopup; ctx.exitPopupSelectedButton = &exitPopupSelectedButton; + ctx.playerName = &playerName; + ctx.fullscreenFlag = &isFullscreen; + ctx.applyFullscreen = [window, &isFullscreen](bool enable) { + SDL_SetWindowFullscreen(window, enable ? SDL_WINDOW_FULLSCREEN : 0); + isFullscreen = enable; + }; + ctx.queryFullscreen = [window]() -> bool { + return (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0; + }; + ctx.requestQuit = [&running]() { + running = false; + }; // Instantiate state objects auto loadingState = std::make_unique(ctx); auto menuState = std::make_unique(ctx); + auto optionsState = std::make_unique(ctx); auto levelSelectorState = std::make_unique(ctx); auto playingState = std::make_unique(ctx); @@ -662,6 +677,10 @@ int main(int, char **) stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); }); stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); }); + stateMgr.registerHandler(AppState::Options, [&](const SDL_Event& e){ optionsState->handleEvent(e); }); + stateMgr.registerOnEnter(AppState::Options, [&](){ optionsState->onEnter(); }); + stateMgr.registerOnExit(AppState::Options, [&](){ optionsState->onExit(); }); + stateMgr.registerHandler(AppState::LevelSelector, [&](const SDL_Event& e){ levelSelectorState->handleEvent(e); }); stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); }); stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->onExit(); }); @@ -773,21 +792,30 @@ int main(int, char **) float btnCX = LOGICAL_W * 0.5f + contentOffsetX; const float btnYOffset = 40.0f; // must match MenuState offset float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; - SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; - SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; + float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; + std::array buttonRects{}; + for (int i = 0; i < 4; ++i) { + float center = btnCX + (static_cast(i) - 1.5f) * spacing; + buttonRects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH}; + } - if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) - { - // Reset the game first with the chosen start level so HUD and - // Playing state see the correct 0-based level immediately. + auto pointInRect = [&](const SDL_FRect& r) { + return lx >= r.x && lx <= r.x + r.w && ly >= r.y && ly <= r.y + r.h; + }; + + if (pointInRect(buttonRects[0])) { game.reset(startLevelSelection); state = AppState::Playing; stateMgr.setState(state); - } - else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) - { + } else if (pointInRect(buttonRects[1])) { state = AppState::LevelSelector; stateMgr.setState(state); + } else if (pointInRect(buttonRects[2])) { + state = AppState::Options; + stateMgr.setState(state); + } else if (pointInRect(buttonRects[3])) { + showExitConfirmPopup = true; + exitPopupSelectedButton = 1; } // Settings button (gear icon area - top right) @@ -846,6 +874,32 @@ int main(int, char **) game.setPaused(false); } } + else if (state == AppState::Menu && showExitConfirmPopup) { + float contentW = LOGICAL_W * logicalScale; + float contentH = LOGICAL_H * logicalScale; + float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; + float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; + float popupW = 420.0f; + float popupH = 230.0f; + float popupX = (LOGICAL_W - popupW) * 0.5f + contentOffsetX; + float popupY = (LOGICAL_H - popupH) * 0.5f + contentOffsetY; + float btnW = 140.0f; + float btnH = 50.0f; + float yesX = popupX + popupW * 0.3f - btnW / 2.0f; + float noX = popupX + popupW * 0.7f - btnW / 2.0f; + float btnY = popupY + popupH - btnH - 30.0f; + bool insidePopup = lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH; + if (insidePopup) { + if (lx >= yesX && lx <= yesX + btnW && ly >= btnY && ly <= btnY + btnH) { + showExitConfirmPopup = false; + running = false; + } else if (lx >= noX && lx <= noX + btnW && ly >= btnY && ly <= btnY + btnH) { + showExitConfirmPopup = false; + } + } else { + showExitConfirmPopup = false; + } + } } } else if (e.type == SDL_EVENT_MOUSE_MOTION) @@ -867,15 +921,16 @@ int main(int, char **) float btnCX = LOGICAL_W * 0.5f + contentOffsetX; const float btnYOffset = 40.0f; // must match MenuState offset float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset; - SDL_FRect playBtn{btnCX - btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; - SDL_FRect levelBtn{btnCX + btnW * 0.6f - btnW/2.0f, btnCY - btnH/2.0f, btnW, btnH}; - - // Check menu button hovers (no level popup to handle anymore) + float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; hoveredButton = -1; - if (lx >= playBtn.x && lx <= playBtn.x + playBtn.w && ly >= playBtn.y && ly <= playBtn.y + playBtn.h) - hoveredButton = 0; - else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) - hoveredButton = 1; + for (int i = 0; i < 4; ++i) { + float center = btnCX + (static_cast(i) - 1.5f) * spacing; + SDL_FRect rect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH}; + if (lx >= rect.x && lx <= rect.x + rect.w && ly >= rect.y && ly <= rect.y + rect.h) { + hoveredButton = i; + break; + } + } } } } diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 39c1364..6f55c26 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include // Use dynamic logical dimensions from GlobalState instead of hardcoded values @@ -22,38 +23,115 @@ 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::onExit() { + if (ctx.showExitConfirmPopup) { + *ctx.showExitConfirmPopup = false; + } } void MenuState::handleEvent(const SDL_Event& e) { // Keyboard navigation for menu buttons if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { + 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; + } + } + switch (e.key.scancode) { case SDL_SCANCODE_LEFT: case SDL_SCANCODE_UP: - selectedButton = 0; // PLAY + { + const int total = 4; + selectedButton = (selectedButton + total - 1) % total; break; + } case SDL_SCANCODE_RIGHT: case SDL_SCANCODE_DOWN: - selectedButton = 1; // LEVEL + { + const int total = 4; + selectedButton = (selectedButton + 1) % total; break; + } case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: case SDL_SCANCODE_SPACE: - // Activate selected button - if (selectedButton == 0) { - // PLAY button - transition to Playing state - if (ctx.stateManager) { - ctx.stateManager->setState(AppState::Playing); - } - } else { - // LEVEL button - transition to LevelSelector state - if (ctx.stateManager) { - ctx.stateManager->setState(AppState::LevelSelector); - } + if (!ctx.stateManager) { + break; } + switch (selectedButton) { + case 0: + ctx.stateManager->setState(AppState::Playing); + break; + case 1: + ctx.stateManager->setState(AppState::LevelSelector); + break; + case 2: + ctx.stateManager->setState(AppState::Options); + break; + case 3: + setExitPrompt(true); + setExitSelection(1); + break; + } + break; + case SDL_SCANCODE_ESCAPE: + setExitPrompt(true); + setExitSelection(1); break; default: break; @@ -204,32 +282,88 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi char levelBtnText[32]; int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0; std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel); - // Draw simple styled buttons (replicating menu_drawMenuButton) + auto drawMenuButtonLocal = [&](SDL_Renderer* r, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, SDL_Color bg, SDL_Color border, bool selected){ float x = cx - w/2; float y = cy - h/2; - - // If selected, draw a glow effect if (selected) { - SDL_SetRenderDrawColor(r, 255, 220, 0, 100); + SDL_SetRenderDrawColor(r, 255, 220, 0, 110); SDL_FRect glow{ x-10, y-10, w+20, h+20 }; SDL_RenderFillRect(r, &glow); } - SDL_SetRenderDrawColor(r, border.r, border.g, border.b, border.a); SDL_FRect br{ x-6, y-6, w+12, h+12 }; SDL_RenderFillRect(r, &br); SDL_SetRenderDrawColor(r, 255,255,255,255); SDL_FRect br2{ x-4, y-4, w+8, h+8 }; SDL_RenderFillRect(r, &br2); SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a); SDL_FRect br3{ x, y, w, h }; SDL_RenderFillRect(r, &br3); - float textScale = 1.6f; float approxCharW = 12.0f * textScale; float textW = label.length() * approxCharW; float tx = x + (w - textW) / 2.0f; float ty = y + (h - 20.0f * textScale) / 2.0f; - font.draw(r, tx+2, ty+2, label, textScale, SDL_Color{0,0,0,180}); + float textScale = 1.5f; float approxCharW = 12.0f * textScale; float textW = label.length() * approxCharW; float tx = x + (w - textW) / 2.0f; float ty = y + (h - 20.0f * textScale) / 2.0f; + font.draw(r, tx+2, ty+2, label, textScale, SDL_Color{0,0,0,200}); font.draw(r, tx, ty, label, textScale, SDL_Color{255,255,255,255}); }; - drawMenuButtonLocal(renderer, *ctx.pixelFont, btnX - btnW * 0.6f, btnY, btnW, btnH, std::string("PLAY"), SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}, selectedButton == 0); - { + + 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" } + }; + + float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f; + for (size_t i = 0; i < buttons.size(); ++i) { + float offset = (static_cast(i) - 1.5f) * spacing; + float cx = btnX + offset; + drawMenuButtonLocal(renderer, *ctx.pixelFont, cx, btnY, btnW, btnH, buttons[i].label, buttons[i].bg, buttons[i].border, selectedButton == static_cast(i)); } - drawMenuButtonLocal(renderer, *ctx.pixelFont, btnX + btnW * 0.6f, btnY, btnW, btnH, std::string(levelBtnText), SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}, selectedButton == 1); - { - FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after draw LEVEL button\n"); fclose(f); } + } + + if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) { + int selection = ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1; + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 150); + SDL_FRect overlay{contentOffsetX, contentOffsetY, LOGICAL_W, LOGICAL_H}; + SDL_RenderFillRect(renderer, &overlay); + + float popupW = 420.0f; + float popupH = 230.0f; + float popupX = (LOGICAL_W - popupW) * 0.5f + contentOffsetX; + float popupY = (LOGICAL_H - popupH) * 0.5f + contentOffsetY; + + SDL_SetRenderDrawColor(renderer, 20, 30, 50, 240); + SDL_FRect popup{popupX, popupY, popupW, popupH}; + SDL_RenderFillRect(renderer, &popup); + SDL_SetRenderDrawColor(renderer, 90, 140, 220, 255); + SDL_RenderRect(renderer, &popup); + + FontAtlas* titleFont = ctx.font ? ctx.font : ctx.pixelFont; + if (titleFont) { + titleFont->draw(renderer, popupX + 40.0f, popupY + 30.0f, "EXIT GAME?", 1.8f, SDL_Color{255, 230, 140, 255}); + titleFont->draw(renderer, popupX + 40.0f, popupY + 80.0f, "Are you sure you want to quit?", 1.1f, SDL_Color{200, 210, 230, 255}); } + + auto drawChoice = [&](const char* label, float cx, int idx) { + float btnW2 = 140.0f; + float btnH2 = 50.0f; + float x = cx - btnW2 / 2.0f; + float y = popupY + popupH - btnH2 - 30.0f; + bool selected = (selection == idx); + SDL_Color bg = selected ? SDL_Color{220, 180, 60, 255} : SDL_Color{80, 110, 160, 255}; + SDL_Color border = selected ? SDL_Color{255, 220, 120, 255} : SDL_Color{40, 60, 100, 255}; + SDL_SetRenderDrawColor(renderer, border.r, border.g, border.b, border.a); + SDL_FRect br{ x-4, y-4, btnW2+8, btnH2+8 }; + SDL_RenderFillRect(renderer, &br); + SDL_SetRenderDrawColor(renderer, bg.r, bg.g, bg.b, bg.a); + SDL_FRect body{ x, y, btnW2, btnH2 }; + SDL_RenderFillRect(renderer, &body); + if (titleFont) { + titleFont->draw(renderer, x + 20.0f, y + 10.0f, label, 1.2f, SDL_Color{15, 20, 35, 255}); + } + }; + drawChoice("YES", popupX + popupW * 0.3f, 0); + drawChoice("NO", popupX + popupW * 0.7f, 1); } // Popups (settings only - level popup is now a separate state) diff --git a/src/states/MenuState.h b/src/states/MenuState.h index 88c500d..c8949f5 100644 --- a/src/states/MenuState.h +++ b/src/states/MenuState.h @@ -12,5 +12,5 @@ public: void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override; private: - int selectedButton = 0; // 0 = PLAY, 1 = LEVEL + int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = EXIT }; diff --git a/src/states/OptionsState.cpp b/src/states/OptionsState.cpp new file mode 100644 index 0000000..67de3c7 --- /dev/null +++ b/src/states/OptionsState.cpp @@ -0,0 +1,238 @@ +#include "OptionsState.h" +#include "../core/state/StateManager.h" +#include "../graphics/ui/Font.h" +#include +#include + +OptionsState::OptionsState(StateContext& ctx) : State(ctx) {} + +void OptionsState::onEnter() { + m_selectedField = Field::PlayerName; + m_cursorTimer = 0.0; + m_cursorVisible = true; + if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) { + SDL_StartTextInput(focusWin); + } +} + +void OptionsState::onExit() { + if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) { + SDL_StopTextInput(focusWin); + } +} + +void OptionsState::handleEvent(const SDL_Event& e) { + if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { + switch (e.key.scancode) { + case SDL_SCANCODE_ESCAPE: + exitToMenu(); + return; + case SDL_SCANCODE_UP: + case SDL_SCANCODE_W: + moveSelection(-1); + return; + case SDL_SCANCODE_DOWN: + case SDL_SCANCODE_S: + moveSelection(1); + return; + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + case SDL_SCANCODE_SPACE: + activateSelection(); + return; + case SDL_SCANCODE_LEFT: + case SDL_SCANCODE_RIGHT: + if (m_selectedField == Field::Fullscreen) { + toggleFullscreen(); + return; + } + break; + default: + break; + } + + if (m_selectedField == Field::PlayerName) { + handleNameInput(e); + } + } else if (e.type == SDL_EVENT_TEXT_INPUT && m_selectedField == Field::PlayerName) { + handleNameInput(e); + } +} + +void OptionsState::update(double frameMs) { + m_cursorTimer += frameMs; + if (m_cursorTimer >= 450.0) { + m_cursorTimer = 0.0; + m_cursorVisible = !m_cursorVisible; + } +} + +void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { + if (!renderer) return; + + const float LOGICAL_W = 1200.0f; + const float LOGICAL_H = 1000.0f; + + float winW = static_cast(logicalVP.w); + float winH = static_cast(logicalVP.h); + float contentW = LOGICAL_W * logicalScale; + float contentH = LOGICAL_H * logicalScale; + float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; + float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140); + SDL_FRect dim{contentOffsetX, contentOffsetY, LOGICAL_W, LOGICAL_H}; + SDL_RenderFillRect(renderer, &dim); + + const float panelW = 560.0f; + const float panelH = 420.0f; + SDL_FRect panel{ + (LOGICAL_W - panelW) * 0.5f + contentOffsetX, + (LOGICAL_H - panelH) * 0.5f + contentOffsetY, + panelW, + panelH + }; + + SDL_SetRenderDrawColor(renderer, 15, 20, 34, 230); + SDL_RenderFillRect(renderer, &panel); + SDL_SetRenderDrawColor(renderer, 70, 110, 190, 255); + SDL_RenderRect(renderer, &panel); + + FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; + FontAtlas* bodyFont = ctx.font ? ctx.font : ctx.pixelFont; + + auto drawText = [&](FontAtlas* font, float x, float y, const std::string& text, float scale, SDL_Color color) { + if (!font) return; + font->draw(renderer, x, y, text, scale, color); + }; + + drawText(titleFont, panel.x + 24.0f, panel.y + 24.0f, "OPTIONS", 2.0f, {255, 230, 120, 255}); + + auto drawField = [&](Field field, float y, const std::string& label, const std::string& value) { + bool selected = (field == m_selectedField); + SDL_FRect row{panel.x + 20.0f, y - 10.0f, panel.w - 40.0f, 70.0f}; + SDL_SetRenderDrawColor(renderer, selected ? 40 : 24, selected ? 80 : 36, selected ? 120 : 48, 220); + SDL_RenderFillRect(renderer, &row); + SDL_SetRenderDrawColor(renderer, 80, 120, 200, 255); + SDL_RenderRect(renderer, &row); + + drawText(bodyFont, row.x + 18.0f, row.y + 12.0f, label, 1.4f, {200, 220, 255, 255}); + drawText(bodyFont, row.x + 18.0f, row.y + 36.0f, value, 1.6f, {255, 255, 255, 255}); + }; + + std::string nameDisplay = playerName(); + if (nameDisplay.empty()) { + nameDisplay = ""; + } + if (m_selectedField == Field::PlayerName && m_cursorVisible) { + nameDisplay.push_back('_'); + } + + drawField(Field::PlayerName, panel.y + 90.0f, "PLAYER NAME", nameDisplay); + + std::string fullscreenValue = isFullscreen() ? "ON" : "OFF"; + drawField(Field::Fullscreen, panel.y + 180.0f, "FULLSCREEN", fullscreenValue); + + drawField(Field::Back, panel.y + 270.0f, "BACK", "RETURN TO MENU"); + + drawText(bodyFont, panel.x + 24.0f, panel.y + panel.h - 50.0f, + "ARROWS = NAV ENTER = SELECT ESC = MENU", 1.1f, {190, 200, 215, 255}); + drawText(bodyFont, panel.x + 24.0f, panel.y + panel.h - 26.0f, + "LETTERS/NUMBERS TYPE INTO NAME FIELD", 1.0f, {150, 160, 180, 255}); +} + +void OptionsState::moveSelection(int delta) { + int idx = static_cast(m_selectedField); + int total = 3; + idx = (idx + delta + total) % total; + m_selectedField = static_cast(idx); +} + +void OptionsState::activateSelection() { + switch (m_selectedField) { + case Field::PlayerName: + // Nothing to do; typing is always enabled + break; + case Field::Fullscreen: + toggleFullscreen(); + break; + case Field::Back: + exitToMenu(); + break; + } +} + +void OptionsState::handleNameInput(const SDL_Event& e) { + if (!ctx.playerName) return; + + if (e.type == SDL_EVENT_KEY_DOWN) { + if (e.key.scancode == SDL_SCANCODE_BACKSPACE) { + removeCharacter(); + } else if (e.key.scancode == SDL_SCANCODE_SPACE) { + addCharacter(' '); + } else { + SDL_Keymod mods = SDL_GetModState(); + SDL_Keycode keycode = SDL_GetKeyFromScancode(e.key.scancode, mods, true); + bool shift = (mods & SDL_KMOD_SHIFT) != 0; + char c = static_cast(keycode); + if (keycode >= 'a' && keycode <= 'z') { + c = shift ? static_cast(std::toupper(c)) : static_cast(std::toupper(c)); + addCharacter(c); + } else if (keycode >= '0' && keycode <= '9') { + addCharacter(static_cast(keycode)); + } + } + } else if (e.type == SDL_EVENT_TEXT_INPUT) { + const char* text = e.text.text; + while (*text) { + unsigned char c = static_cast(*text); + if (std::isalnum(c) || c == ' ') { + addCharacter(static_cast(std::toupper(c))); + } + ++text; + } + } +} + +void OptionsState::addCharacter(char c) { + if (!ctx.playerName) return; + if (c == '\0') return; + if (c == ' ' && ctx.playerName->empty()) return; + if (ctx.playerName->size() >= MAX_NAME_LENGTH) return; + + ctx.playerName->push_back(c); +} + +void OptionsState::removeCharacter() { + if (!ctx.playerName || ctx.playerName->empty()) return; + ctx.playerName->pop_back(); +} + +void OptionsState::toggleFullscreen() { + bool nextState = !isFullscreen(); + if (ctx.applyFullscreen) { + ctx.applyFullscreen(nextState); + } + if (ctx.fullscreenFlag) { + *ctx.fullscreenFlag = nextState; + } +} + +void OptionsState::exitToMenu() { + if (ctx.stateManager) { + ctx.stateManager->setState(AppState::Menu); + } +} + +const std::string& OptionsState::playerName() const { + static std::string empty; + return ctx.playerName ? *ctx.playerName : empty; +} + +bool OptionsState::isFullscreen() const { + if (ctx.queryFullscreen) { + return ctx.queryFullscreen(); + } + return ctx.fullscreenFlag ? *ctx.fullscreenFlag : false; +} diff --git a/src/states/OptionsState.h b/src/states/OptionsState.h new file mode 100644 index 0000000..f6285b4 --- /dev/null +++ b/src/states/OptionsState.h @@ -0,0 +1,35 @@ +#pragma once + +#include "State.h" + +class OptionsState : public State { +public: + explicit OptionsState(StateContext& ctx); + void onEnter() override; + void onExit() override; + void handleEvent(const SDL_Event& e) override; + void update(double frameMs) override; + void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override; + +private: + enum class Field : int { + PlayerName = 0, + Fullscreen = 1, + Back = 2 + }; + + static constexpr int MAX_NAME_LENGTH = 12; + Field m_selectedField = Field::PlayerName; + double m_cursorTimer = 0.0; + bool m_cursorVisible = true; + + void moveSelection(int delta); + void activateSelection(); + void handleNameInput(const SDL_Event& e); + void addCharacter(char c); + void removeCharacter(); + void toggleFullscreen(); + void exitToMenu(); + const std::string& playerName() const; + bool isFullscreen() const; +}; diff --git a/src/states/State.h b/src/states/State.h index cc8d7cc..d6146b2 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include // Forward declarations for frequently used types class Game; @@ -48,6 +50,11 @@ struct StateContext { bool* showSettingsPopup = nullptr; bool* showExitConfirmPopup = nullptr; // If true, show "Exit game?" confirmation while playing int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default) + std::string* playerName = nullptr; // Shared player name buffer for highscores/options + bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available + 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 // Pointer to the application's StateManager so states can request transitions StateManager* stateManager = nullptr; };