Created LevelSelectorState
- code removed from main.cpp and added into a new class
This commit is contained in:
@ -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);
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
enum class AppState {
|
||||
Loading,
|
||||
Menu,
|
||||
LevelSelector,
|
||||
Playing,
|
||||
LevelSelect,
|
||||
GameOver
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
207
src/main.cpp
207
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<LoadingState>(ctx);
|
||||
auto menuState = std::make_unique<MenuState>(ctx);
|
||||
auto levelSelectorState = std::make_unique<LevelSelectorState>(ctx);
|
||||
auto playingState = std::make_unique<PlayingState>(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:
|
||||
{
|
||||
|
||||
273
src/states/LevelSelectorState.cpp
Normal file
273
src/states/LevelSelectorState.cpp
Normal file
@ -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 <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
25
src/states/LevelSelectorState.h
Normal file
25
src/states/LevelSelectorState.h
Normal file
@ -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();
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user