Created LevelSelectorState

- code removed from main.cpp and added into a new class
This commit is contained in:
2025-08-17 09:10:49 +02:00
parent eddd1a24b2
commit 0e0519b0e9
9 changed files with 346 additions and 173 deletions

View 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);
}
}

View 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();
};

View File

@ -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);
}

View File

@ -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