diff --git a/CMakeLists.txt b/CMakeLists.txt index cadd58c..4de4bb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ add_executable(tetris # State implementations (new) src/states/LoadingState.cpp src/states/MenuState.cpp + src/states/LevelSelectorState.cpp src/states/PlayingState.cpp ) diff --git a/src/audio/MenuWrappers.h b/src/audio/MenuWrappers.h index 68847af..08a2d66 100644 --- a/src/audio/MenuWrappers.h +++ b/src/audio/MenuWrappers.h @@ -18,5 +18,4 @@ void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, void menu_drawMenuButton(SDL_Renderer* renderer, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, SDL_Color bgColor, SDL_Color borderColor); -void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel); void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled); diff --git a/src/core/StateManager.h b/src/core/StateManager.h index 8f9bcf1..c2bacde 100644 --- a/src/core/StateManager.h +++ b/src/core/StateManager.h @@ -9,6 +9,7 @@ enum class AppState { Loading, Menu, + LevelSelector, Playing, LevelSelect, GameOver diff --git a/src/gameplay/Game.cpp b/src/gameplay/Game.cpp index 6255150..3a3ea14 100644 --- a/src/gameplay/Game.cpp +++ b/src/gameplay/Game.cpp @@ -37,11 +37,13 @@ namespace { double result = frames * FRAME_MS * globalMultiplier; static int debug_calls = 0; + /* if (debug_calls < 3) { printf("Level %d: %d frames per cell (mult %.2f) = %.1f ms per cell (global x%.2f)\\n", level, lg.framesPerCell, lg.levelMultiplier, result, globalMultiplier); debug_calls++; } + */ return result; } } diff --git a/src/main.cpp b/src/main.cpp index 4a4e008..2992cf2 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/LevelSelectorState.h" #include "states/PlayingState.h" #include "audio/MenuWrappers.h" @@ -72,7 +73,7 @@ static void drawRect(SDL_Renderer *r, float x, float y, float w, float h, SDL_Co } // Hover state for level popup ( -1 = none, 0..19 = hovered level ) -static int hoveredLevel = -1; +// Now managed by LevelSelectorState // ...existing code... @@ -106,13 +107,8 @@ void menu_drawEnhancedButton(SDL_Renderer* renderer, FontAtlas& font, float cx, // Popup wrappers // Forward declarations for popup functions defined later in this file -static void drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel); static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled); -void menu_drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel) { - drawLevelSelectionPopup(renderer, font, bgTex, selectedLevel); -} - void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) { drawSettingsPopup(renderer, font, musicEnabled); } @@ -230,77 +226,6 @@ static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, Piece // ----------------------------------------------------------------------------- // Popup Drawing Functions // ----------------------------------------------------------------------------- -static void drawLevelSelectionPopup(SDL_Renderer* renderer, FontAtlas& font, SDL_Texture* bgTex, int selectedLevel) { - // Popup dims scale with logical size for responsiveness - float popupW = 400, popupH = 300; - float popupX = (LOGICAL_W - popupW) / 2; - float popupY = (LOGICAL_H - popupH) / 2; - - // Draw the background picture stretched to full logical viewport if available - if (bgTex) { - // Dim the background by rendering it then overlaying a semi-transparent black rect - SDL_FRect dst{0, 0, (float)LOGICAL_W, (float)LOGICAL_H}; - SDL_RenderTexture(renderer, bgTex, nullptr, &dst); - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160); - SDL_FRect dim{0,0,(float)LOGICAL_W,(float)LOGICAL_H}; - SDL_RenderFillRect(renderer, &dim); - } else { - // Fallback to semi-transparent overlay - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); - SDL_FRect overlay{0, 0, (float)LOGICAL_W, (float)LOGICAL_H}; - SDL_RenderFillRect(renderer, &overlay); - } - - // Popup panel with border and subtle background - drawRect(renderer, popupX-6, popupY-6, popupW+12, popupH+12, {90, 110, 140, 200}); // outer border - drawRect(renderer, popupX-3, popupY-3, popupW+6, popupH+6, {30, 38, 60, 220}); // inner border - drawRect(renderer, popupX, popupY, popupW, popupH, {18, 22, 34, 235}); // panel - - // Title (use retro pixel font) - font.draw(renderer, popupX + 28, popupY + 18, "SELECT STARTING LEVEL", 2.4f, {255, 220, 0, 255}); - - // Grid layout for levels: 4 columns x 5 rows - int cols = 4, rows = 5; - float padding = 24.0f; - float gridW = popupW - padding * 2; - float gridH = popupH - 120.0f; // leave space for title and instructions - float cellW = gridW / cols; - float cellH = std::min(80.0f, gridH / rows - 12.0f); - - float gridStartX = popupX + padding; - float gridStartY = popupY + 70; - - for (int level = 0; level < 20; ++level) { - int row = level / cols; - int col = level % cols; - float cx = gridStartX + col * cellW; - float cy = gridStartY + row * (cellH + 12.0f); - - bool isSelected = (level == selectedLevel); - SDL_Color bg = isSelected ? SDL_Color{255, 220, 0, 255} : SDL_Color{70, 85, 120, 240}; - SDL_Color fg = isSelected ? SDL_Color{0, 0, 0, 255} : SDL_Color{240, 240, 245, 255}; - - // Button background - // Hover highlight - bool isHovered = (level == hoveredLevel); - if (isHovered && !isSelected) { - // slightly brighter hover background - SDL_Color hoverBg = SDL_Color{95, 120, 170, 255}; - drawRect(renderer, cx + 8 - 2, cy - 2, cellW - 12, cellH + 4, hoverBg); - } - drawRect(renderer, cx + 8, cy, cellW - 16, cellH, bg); - - // Level label centered - char levelStr[8]; snprintf(levelStr, sizeof(levelStr), "%d", level); - float tx = cx + (cellW / 2.0f) - (6.0f * 1.8f); // rough centering - float ty = cy + (cellH / 2.0f) - 10.0f; - font.draw(renderer, tx, ty, levelStr, 1.8f, fg); - } - - // Instructions under grid - font.draw(renderer, popupX + 28, popupY + popupH - 40, "CLICK A LEVEL TO SELECT • ESC = CANCEL", 1.0f, {200,200,220,255}); -} - static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicEnabled) { float popupW = 350, popupH = 260; float popupX = (LOGICAL_W - popupW) / 2; @@ -349,7 +274,6 @@ static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musi // Intro/Menu state variables // ----------------------------------------------------------------------------- static double logoAnimCounter = 0.0; -static bool showLevelPopup = false; static bool showSettingsPopup = false; static bool showExitConfirmPopup = false; static bool musicEnabled = true; @@ -718,13 +642,13 @@ int main(int, char **) ctx.musicEnabled = &musicEnabled; ctx.startLevelSelection = &startLevelSelection; ctx.hoveredButton = &hoveredButton; - ctx.showLevelPopup = &showLevelPopup; ctx.showSettingsPopup = &showSettingsPopup; ctx.showExitConfirmPopup = &showExitConfirmPopup; // Instantiate state objects auto loadingState = std::make_unique(ctx); auto menuState = std::make_unique(ctx); + auto levelSelectorState = std::make_unique(ctx); auto playingState = std::make_unique(ctx); // Register handlers and lifecycle hooks @@ -736,6 +660,10 @@ int main(int, char **) stateMgr.registerOnEnter(AppState::Menu, [&](){ menuState->onEnter(); }); stateMgr.registerOnExit(AppState::Menu, [&](){ menuState->onExit(); }); + stateMgr.registerHandler(AppState::LevelSelector, [&](const SDL_Event& e){ levelSelectorState->handleEvent(e); }); + stateMgr.registerOnEnter(AppState::LevelSelector, [&](){ levelSelectorState->onEnter(); }); + stateMgr.registerOnExit(AppState::LevelSelector, [&](){ levelSelectorState->onExit(); }); + // Combined Playing state handler: run playingState handler and inline gameplay mapping stateMgr.registerHandler(AppState::Playing, [&](const SDL_Event& e){ // First give the PlayingState a chance to handle the event @@ -832,41 +760,7 @@ int main(int, char **) float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; - if (showLevelPopup) { - // Handle level selection popup clicks (use same math as drawLevelSelectionPopup) - float popupW = 400, popupH = 300; - float popupX = (LOGICAL_W - popupW) / 2; - float popupY = (LOGICAL_H - popupH) / 2; - - int cols = 4, rows = 5; - float padding = 24.0f; - float gridW = popupW - padding * 2; - float gridH = popupH - 120.0f; - float cellW = gridW / cols; - float cellH = std::min(80.0f, gridH / rows - 12.0f); - float gridStartX = popupX + padding; - float gridStartY = popupY + 70; - - if (lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH) { - // Click inside popup - check level grid - if (lx >= gridStartX && ly >= gridStartY) { - int col = int((lx - gridStartX) / cellW); - int row = int((ly - gridStartY) / (cellH + 12.0f)); - if (col >= 0 && col < cols && row >= 0 && row < rows) { - int selectedLevel = row * cols + col; - if (selectedLevel < 20) { - startLevelSelection = selectedLevel; - showLevelPopup = false; - hoveredLevel = -1; - } - } - } - } else { - // Click outside popup - close it - showLevelPopup = false; - hoveredLevel = -1; - } - } else if (showSettingsPopup) { + if (showSettingsPopup) { // Click anywhere closes settings popup showSettingsPopup = false; } else { @@ -890,7 +784,8 @@ int main(int, char **) } else if (lx >= levelBtn.x && lx <= levelBtn.x + levelBtn.w && ly >= levelBtn.y && ly <= levelBtn.y + levelBtn.h) { - showLevelPopup = true; + state = AppState::LevelSelector; + stateMgr.setState(state); } // Settings button (gear icon area - top right) @@ -921,24 +816,24 @@ int main(int, char **) float localY = ly - contentOffsetY; // Popup rect in logical coordinates (content-local) - float popupW = 420, popupH = 180; - float popupX = (LOGICAL_W - popupW) / 2; - float popupY = (LOGICAL_H - popupH) / 2; + float popupW = 400, popupH = 200; + float popupX = (LOGICAL_W - popupW) / 2.0f; + float popupY = (LOGICAL_H - popupH) / 2.0f; + // Simple Yes/No buttons + float btnW = 120.0f, btnH = 40.0f; + float yesX = popupX + popupW * 0.25f - btnW / 2.0f; + float noX = popupX + popupW * 0.75f - btnW / 2.0f; + float btnY = popupY + popupH - btnH - 20.0f; if (localX >= popupX && localX <= popupX + popupW && localY >= popupY && localY <= popupY + popupH) { - // Inside popup: two buttons Yes / No - float btnW = 140, btnH = 46; - float yesX = popupX + popupW * 0.25f - btnW/2.0f; - float noX = popupX + popupW * 0.75f - btnW/2.0f; - float btnY = popupY + popupH - 60; + // Click inside popup - check buttons if (localX >= yesX && localX <= yesX + btnW && localY >= btnY && localY <= btnY + btnH) { // Yes -> go back to menu showExitConfirmPopup = false; game.reset(startLevelSelection); state = AppState::Menu; stateMgr.setState(state); - } - else if (localX >= noX && localX <= noX + btnW && localY >= btnY && localY <= btnY + btnH) { + } else if (localX >= noX && localX <= noX + btnW && localY >= btnY && localY <= btnY + btnH) { // No -> close popup and resume showExitConfirmPopup = false; game.setPaused(false); @@ -973,40 +868,12 @@ int main(int, char **) 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}; - // If level popup is not shown, clear hoveredLevel; otherwise compute it + // Check menu button hovers (no level popup to handle anymore) hoveredButton = -1; - if (!showLevelPopup) { - 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; - hoveredLevel = -1; - } else { - // compute hover over popup grid using same math as drawLevelSelectionPopup - float popupW = 400, popupH = 300; - float popupX = (LOGICAL_W - popupW) / 2; - float popupY = (LOGICAL_H - popupH) / 2; - int cols = 4, rows = 5; - float padding = 24.0f; - float gridW = popupW - padding * 2; - float gridH = popupH - 120.0f; - float cellW = gridW / cols; - float cellH = std::min(80.0f, gridH / rows - 12.0f); - float gridStartX = popupX + padding; - float gridStartY = popupY + 70; - - hoveredLevel = -1; - if (lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH) { - if (lx >= gridStartX && ly >= gridStartY) { - int col = int((lx - gridStartX) / cellW); - int row = int((ly - gridStartY) / (cellH + 12.0f)); - if (col >= 0 && col < cols && row >= 0 && row < rows) { - int sel = row * cols + col; - if (sel < 20) hoveredLevel = sel; - } - } - } - } + 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; } } } @@ -1186,6 +1053,9 @@ int main(int, char **) case AppState::Menu: menuState->update(frameMs); break; + case AppState::LevelSelector: + levelSelectorState->update(frameMs); + break; case AppState::Playing: playingState->update(frameMs); break; @@ -1363,14 +1233,23 @@ int main(int, char **) // Delegate full menu rendering to MenuState object now menuState->render(renderer, logicalScale, logicalVP); break; + case AppState::LevelSelector: + // Delegate level selector rendering to LevelSelectorState + levelSelectorState->render(renderer, logicalScale, logicalVP); + break; case AppState::LevelSelect: - font.draw(renderer, LOGICAL_W * 0.5f - 120, 80, "SELECT LEVEL", 2.5f, SDL_Color{255, 220, 0, 255}); - { - char buf[64]; - std::snprintf(buf, sizeof(buf), "LEVEL: %d", startLevelSelection); - font.draw(renderer, LOGICAL_W * 0.5f - 80, 180, buf, 2.0f, SDL_Color{200, 240, 255, 255}); - } + { + const std::string title = "SELECT LEVEL"; + int tW = 0, tH = 0; + font.measure(title, 2.5f, tW, tH); + float titleX = (LOGICAL_W - (float)tW) / 2.0f; + font.draw(renderer, titleX, 80, title, 2.5f, SDL_Color{255, 220, 0, 255}); + + char buf[64]; + std::snprintf(buf, sizeof(buf), "LEVEL: %d", startLevelSelection); + font.draw(renderer, LOGICAL_W * 0.5f - 80, 180, buf, 2.0f, SDL_Color{200, 240, 255, 255}); font.draw(renderer, LOGICAL_W * 0.5f - 180, 260, "ARROWS CHANGE ENTER=OK ESC=BACK", 1.2f, SDL_Color{200, 200, 220, 255}); + } break; case AppState::Playing: { diff --git a/src/states/LevelSelectorState.cpp b/src/states/LevelSelectorState.cpp new file mode 100644 index 0000000..450eeb6 --- /dev/null +++ b/src/states/LevelSelectorState.cpp @@ -0,0 +1,273 @@ +// LevelSelectorState.cpp - Level selection popup state implementation +#include "LevelSelectorState.h" +#include "State.h" +#include "../core/StateManager.h" +#include "../graphics/Font.h" +#include +#include +#include + +// Constants from main.cpp +static constexpr int LOGICAL_W = 1200; +static constexpr int LOGICAL_H = 1000; + +// Helper function to draw rectangles (matches main.cpp implementation) +static void drawRect(SDL_Renderer *r, float x, float y, float w, float h, SDL_Color c) +{ + SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); + SDL_FRect fr{x, y, w, h}; + SDL_RenderFillRect(r, &fr); +} + +LevelSelectorState::LevelSelectorState(StateContext& ctx) : State(ctx) { +} + +void LevelSelectorState::onEnter() { + hoveredLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0; +} + +void LevelSelectorState::onExit() { + hoveredLevel = -1; +} + +void LevelSelectorState::handleEvent(const SDL_Event& e) { + if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { + // Arrow key navigation + if (e.key.scancode == SDL_SCANCODE_LEFT) { + hoveredLevel = (hoveredLevel - 1 + 20) % 20; + if (ctx.startLevelSelection) *ctx.startLevelSelection = hoveredLevel; + } else if (e.key.scancode == SDL_SCANCODE_RIGHT) { + hoveredLevel = (hoveredLevel + 1) % 20; + if (ctx.startLevelSelection) *ctx.startLevelSelection = hoveredLevel; + } else if (e.key.scancode == SDL_SCANCODE_UP) { + hoveredLevel = (hoveredLevel - 4 + 20) % 20; + if (ctx.startLevelSelection) *ctx.startLevelSelection = hoveredLevel; + } else if (e.key.scancode == SDL_SCANCODE_DOWN) { + hoveredLevel = (hoveredLevel + 4) % 20; + if (ctx.startLevelSelection) *ctx.startLevelSelection = hoveredLevel; + } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) { + selectLevel(hoveredLevel); + } else if (e.key.scancode == SDL_SCANCODE_ESCAPE) { + closePopup(); + } + } else if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + float mx = (float)e.button.x, my = (float)e.button.y; + + // Convert to logical coordinates + // Note: This is simplified - in practice we'd need the same coordinate conversion as main.cpp + float popupX, popupY, popupW, popupH; + if (isMouseInPopup(mx, my, popupX, popupY, popupW, popupH)) { + int clickedLevel = getLevelFromMouse(mx, my, popupX, popupY, popupW, popupH); + if (clickedLevel >= 0 && clickedLevel < 20) { + selectLevel(clickedLevel); + } + } else { + // Click outside popup - close it + closePopup(); + } + } else if (e.type == SDL_EVENT_MOUSE_MOTION) { + float mx = (float)e.motion.x, my = (float)e.motion.y; + updateHoverFromMouse(mx, my); + } +} + +void LevelSelectorState::update(double frameMs) { + // No continuous updates needed for level selector + (void)frameMs; +} + +void LevelSelectorState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { + (void)logicalScale; + (void)logicalVP; + + drawLevelSelectionPopup(renderer); +} + +void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer) { + if (!ctx.font) return; + + // Popup dims scale with logical size for responsiveness + float popupW = 400, popupH = 300; + float popupX = (LOGICAL_W - popupW) / 2; + float popupY = (LOGICAL_H - popupH) / 2; + + // Compute content offset so popup centers the same way as other UI code + if (SDL_Window* win = SDL_GetRenderWindow(renderer)) { + int vw = 0, vh = 0; + SDL_GetWindowSize(win, &vw, &vh); + if (vw > 0 && vh > 0) { + float contentScale = std::min((float)vw / (float)LOGICAL_W, (float)vh / (float)LOGICAL_H); + float contentOffsetX = (vw - LOGICAL_W * contentScale) / (2.0f * contentScale); + float contentOffsetY = (vh - LOGICAL_H * contentScale) / (2.0f * contentScale); + popupX += contentOffsetX; + popupY += contentOffsetY; + } + } + + // Draw the background picture stretched to full renderer viewport if available + SDL_Rect vp{0,0,LOGICAL_W,LOGICAL_H}; + if (SDL_Window* win = SDL_GetRenderWindow(renderer)) { + int vw = 0, vh = 0; + SDL_GetWindowSize(win, &vw, &vh); + if (vw > 0 && vh > 0) { vp.w = vw; vp.h = vh; } + } + + if (ctx.backgroundTex) { + // Dim the background by rendering it then overlaying a semi-transparent black rect + SDL_FRect dst{0, 0, (float)vp.w, (float)vp.h}; + SDL_RenderTexture(renderer, ctx.backgroundTex, nullptr, &dst); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160); + SDL_FRect dim{0,0,(float)vp.w,(float)vp.h}; + SDL_RenderFillRect(renderer, &dim); + } else { + // Fallback to semi-transparent overlay + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); + SDL_FRect overlay{0, 0, (float)vp.w, (float)vp.h}; + SDL_RenderFillRect(renderer, &overlay); + } + + // Popup panel with border and subtle background + drawRect(renderer, popupX-6, popupY-6, popupW+12, popupH+12, {90, 110, 140, 200}); // outer border + drawRect(renderer, popupX-3, popupY-3, popupW+6, popupH+6, {30, 38, 60, 220}); // inner border + drawRect(renderer, popupX, popupY, popupW, popupH, {18, 22, 34, 235}); // panel + + // Title (centered above the popup) - drawn above the box for clarity + { + const std::string title = "SELECT STARTING LEVEL"; + int titleW = 0, titleH = 0; + ctx.font->measure(title, 2.4f, titleW, titleH); + float titleX = popupX + (popupW - (float)titleW) / 2.0f; + float titleY = popupY - (float)titleH - 28.0f; + ctx.font->draw(renderer, titleX, titleY, title, 2.4f, {255, 220, 0, 255}); + } + + // Grid layout for levels: 4 columns x 5 rows + int cols = 4, rows = 5; + float padding = 32.0f; + float gridW = popupW - padding * 2; + float gridH = popupH - 80.0f; // leave less top space since title is above + float cellW = gridW / cols; + float cellH = std::min(96.0f, gridH / rows - 12.0f); + + // Add margin between cells + float cellMargin = 8.0f; + float actualCellW = cellW - cellMargin; + float actualCellH = cellH - cellMargin; + + // Center the grid horizontally inside the popup and position slightly higher inside the box + float gridStartX = popupX + padding; + float gridStartY = popupY + padding/2.0f; + + int selectedLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0; + + for (int level = 0; level < 20; level++) { + int row = level / cols; + int col = level % cols; + float cx = gridStartX + col * cellW + cellMargin/2.0f; + float cy = gridStartY + row * (cellH + 12.0f) + cellMargin/2.0f; + + bool isSelected = (level == selectedLevel); + bool isHovered = (level == hoveredLevel); + + // Button background with thicker border and inner box for a box-style look + const float btnPadX = 24.0f; // increased padding inside each button horizontally + const float btnPadY = 16.0f; // increased padding vertically + + // Outer border + SDL_Color borderCol = isHovered && !isSelected ? SDL_Color{120,140,190,255} : SDL_Color{40,50,70,200}; + drawRect(renderer, cx, cy, actualCellW, actualCellH, borderCol); + + // Inner background - make selected background match the button size + SDL_Color inner = isSelected ? SDL_Color{255, 220, 0, 255} : SDL_Color{50,60,84,255}; + if (!isSelected && isHovered) inner = SDL_Color{90,110,150,255}; + drawRect(renderer, cx + 2, cy + 2, actualCellW - 4, actualCellH - 4, inner); + + // Level label centered using font.measure for accurate centering + char levelStr[8]; + snprintf(levelStr, sizeof(levelStr), "%d", level); + int txtW = 0, txtH = 0; + ctx.font->measure(levelStr, 1.8f, txtW, txtH); + float tx = cx + (actualCellW - (float)txtW) / 2.0f; + float ty = cy + (actualCellH - (float)txtH) / 2.0f; + + // Text color: black on selected (yellow), white on others + SDL_Color fg = isSelected ? SDL_Color{0, 0, 0, 255} : SDL_Color{240, 240, 245, 255}; + ctx.font->draw(renderer, tx, ty, levelStr, 1.8f, fg); + } + + // Instructions below the popup box (centered) + { + const std::string instr = "CLICK A LEVEL TO SELECT • ESC = CANCEL"; + int instrW = 0, instrH = 0; + ctx.font->measure(instr, 1.0f, instrW, instrH); + float instrX = popupX + (popupW - (float)instrW) / 2.0f; + float instrY = popupY + popupH + 40.0f; // below the box with larger padding + ctx.font->draw(renderer, instrX, instrY, instr, 1.0f, {200,200,220,255}); + } +} + +bool LevelSelectorState::isMouseInPopup(float mouseX, float mouseY, float& popupX, float& popupY, float& popupW, float& popupH) { + popupW = 400; + popupH = 300; + popupX = (LOGICAL_W - popupW) / 2; + popupY = (LOGICAL_H - popupH) / 2; + + // Note: This is simplified coordinate conversion - in practice we'd need the same logic as main.cpp + return mouseX >= popupX && mouseX <= popupX + popupW && mouseY >= popupY && mouseY <= popupY + popupH; +} + +int LevelSelectorState::getLevelFromMouse(float mouseX, float mouseY, float popupX, float popupY, float popupW, float popupH) { + int cols = 4, rows = 5; + float padding = 32.0f; + float gridW = popupW - padding * 2; + float gridH = popupH - 80.0f; + float cellW = gridW / cols; + float cellH = std::min(96.0f, gridH / rows - 12.0f); + float cellMargin = 8.0f; + float actualCellW = cellW - cellMargin; + float actualCellH = cellH - cellMargin; + float gridStartX = popupX + padding; + float gridStartY = popupY + padding/2.0f; + + if (mouseX >= gridStartX && mouseY >= gridStartY) { + int col = int((mouseX - gridStartX) / cellW); + int row = int((mouseY - gridStartY) / (cellH + 12.0f)); + if (col >= 0 && col < cols && row >= 0 && row < rows) { + // Check if mouse is within the actual cell area (accounting for margins) + float cellX = gridStartX + col * cellW + cellMargin/2.0f; + float cellY = gridStartY + row * (cellH + 12.0f) + cellMargin/2.0f; + if (mouseX >= cellX && mouseX <= cellX + actualCellW && + mouseY >= cellY && mouseY <= cellY + actualCellH) { + int level = row * cols + col; + if (level < 20) return level; + } + } + } + return -1; +} + +void LevelSelectorState::updateHoverFromMouse(float mouseX, float mouseY) { + float popupX, popupY, popupW, popupH; + if (isMouseInPopup(mouseX, mouseY, popupX, popupY, popupW, popupH)) { + hoveredLevel = getLevelFromMouse(mouseX, mouseY, popupX, popupY, popupW, popupH); + } else { + hoveredLevel = -1; + } +} + +void LevelSelectorState::selectLevel(int level) { + if (ctx.startLevelSelection) { + *ctx.startLevelSelection = level; + } + // Transition back to menu + if (ctx.stateManager) { + ctx.stateManager->setState(AppState::Menu); + } +} + +void LevelSelectorState::closePopup() { + // Transition back to menu without changing level + if (ctx.stateManager) { + ctx.stateManager->setState(AppState::Menu); + } +} diff --git a/src/states/LevelSelectorState.h b/src/states/LevelSelectorState.h new file mode 100644 index 0000000..ee1f1f7 --- /dev/null +++ b/src/states/LevelSelectorState.h @@ -0,0 +1,25 @@ +// LevelSelectorState.h - Level selection popup state +#pragma once +#include "State.h" + +class LevelSelectorState : public State { +public: + explicit LevelSelectorState(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: + int hoveredLevel = -1; // -1 = none, 0..19 = hovered level + + // Helper functions + void drawLevelSelectionPopup(SDL_Renderer* renderer); + bool isMouseInPopup(float mouseX, float mouseY, float& popupX, float& popupY, float& popupW, float& popupH); + int getLevelFromMouse(float mouseX, float mouseY, float popupX, float popupY, float popupW, float popupH); + void updateHoverFromMouse(float mouseX, float mouseY); + void selectLevel(int level); + void closePopup(); +}; diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index b50928a..77fe5d4 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -138,13 +138,7 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi menu_drawMenuButton(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}); } - // Popups (level/settings) if requested - if (ctx.showLevelPopup && *ctx.showLevelPopup) { - // call wrapper which will internally draw on top of current content - // prefer pixelFont for retro look - FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; - menu_drawLevelSelectionPopup(renderer, *useFont, ctx.backgroundTex, ctx.startLevelSelection ? *ctx.startLevelSelection : 0); - } + // Popups (settings only - level popup is now a separate state) if (ctx.showSettingsPopup && *ctx.showSettingsPopup) { menu_drawSettingsPopup(renderer, *ctx.font, ctx.musicEnabled ? *ctx.musicEnabled : false); } diff --git a/src/states/State.h b/src/states/State.h index f75c924..e1eaf3b 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -43,7 +43,6 @@ struct StateContext { int* startLevelSelection = nullptr; int* hoveredButton = nullptr; // Menu popups (exposed from main) - bool* showLevelPopup = nullptr; bool* showSettingsPopup = nullptr; bool* showExitConfirmPopup = nullptr; // If true, show "Exit game?" confirmation while playing // Pointer to the application's StateManager so states can request transitions