// LevelSelectorState.cpp - Level selection popup state implementation #include "LevelSelectorState.h" #include "State.h" #include "../core/state/StateManager.h" #include "../core/GlobalState.h" #include "../graphics/ui/Font.h" #include #include #include #include #include "../graphics/renderers/UIRenderer.h" // Use dynamic logical dimensions from GlobalState instead of hardcoded values // --- Minimal draw helpers and look-and-feel adapted from the sample --- static inline SDL_Color RGBA(Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255) { return SDL_Color{r, g, b, a}; } // Palette static const SDL_Color COL_BG = {11, 15, 20, 255}; static const SDL_Color COL_PANEL = {20, 40, 60, 255}; static const SDL_Color COL_PANEL_IN = {26, 46, 66, 255}; static const SDL_Color COL_CYAN = {0, 255, 255, 200}; static const SDL_Color COL_CYAN_SO = {0, 255, 255, 32}; static const SDL_Color COL_TILE = {30, 40, 60, 255}; static const SDL_Color COL_TILE_H = {60, 80, 100, 255}; static const SDL_Color COL_TILE_B = {74, 94, 118, 220}; static const SDL_Color COL_NUM = {233, 241, 255, 255}; static const SDL_Color COL_ACCENT = {255, 140, 40, 255}; static const SDL_Color COL_TITLE = {255, 200, 50, 255}; static const SDL_Color COL_FOOTER = {154, 167, 178, 255}; static void FillRect(SDL_Renderer* r, SDL_FRect rc, SDL_Color c) { SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); SDL_RenderFillRect(r, &rc); } static void StrokeRect(SDL_Renderer* r, SDL_FRect rc, SDL_Color c) { SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); SDL_RenderRect(r, &rc); } static void Line(SDL_Renderer* r, float x1, float y1, float x2, float y2, SDL_Color c) { SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); SDL_RenderLine(r, x1, y1, x2, y2); } // Bitmap-like digits for cell numbers (keeps consistent look without relying on font inside cells) static void DrawDigit(SDL_Renderer* r, int num, float cx, float cy) { SDL_SetRenderDrawColor(r, COL_NUM.r, COL_NUM.g, COL_NUM.b, COL_NUM.a); auto glyph = [&](int d, float ox) { static const int map[10][15] = { {1,1,1, 1,0,1, 1,0,1, 1,0,1, 1,1,1}, {0,1,0, 1,1,0, 0,1,0, 0,1,0, 1,1,1}, {1,1,1, 0,0,1, 1,1,1, 1,0,0, 1,1,1}, {1,1,1, 0,0,1, 0,1,1, 0,0,1, 1,1,1}, {1,0,1, 1,0,1, 1,1,1, 0,0,1, 0,0,1}, {1,1,1, 1,0,0, 1,1,1, 0,0,1, 1,1,1}, {1,1,1, 1,0,0, 1,1,1, 1,0,1, 1,1,1}, {1,1,1, 0,0,1, 0,1,0, 0,1,0, 0,1,0}, {1,1,1, 1,0,1, 1,1,1, 1,0,1, 1,1,1}, {1,1,1, 1,0,1, 1,1,1, 0,0,1, 1,1,1} }; float s = 5.f; float gx = cx + ox - 6.f, gy = cy - 12.f; for (int y = 0; y < 5; y++) for (int x = 0; x < 3; x++) if (map[d][y * 3 + x]) { SDL_FRect p{gx + x * s, gy + y * s, s - 1.f, s - 1.f}; SDL_RenderFillRect(r, &p); } }; if (num < 10) glyph(num, 0); else { glyph(num / 10, -8); glyph(num % 10, 8); } } // Centered text using project Font, with optional shadow static void DrawText(SDL_Renderer* r, FontAtlas* font, const std::string& s, float x, float y, float scale, SDL_Color col, bool center = true, bool shadow = true) { if (!font) return; int w = 0, h = 0; font->measure(s, scale, w, h); float tx = x, ty = y; if (center) tx -= (float)w / 2.0f; if (shadow) { font->draw(r, tx + 2.0f, ty + 2.0f, s, scale, {0, 0, 0, 200}); } font->draw(r, tx, ty, s, scale, col); } static void Vignette(SDL_Renderer* r, int w, int h) { int pad = w / 10; FillRect(r, SDL_FRect{0, 0, (float)w, (float)pad}, SDL_Color{0, 0, 0, 140}); FillRect(r, SDL_FRect{0, (float)h - pad, (float)w, (float)pad}, SDL_Color{0, 0, 0, 140}); FillRect(r, SDL_FRect{0, 0, (float)pad, (float)h}, SDL_Color{0, 0, 0, 140}); FillRect(r, SDL_FRect{(float)w - pad, 0, (float)pad, (float)h}, SDL_Color{0, 0, 0, 140}); } // DrawPanel removed, replaced by UIRenderer::drawSciFiPanel struct Grid { int cols = 4, rows = 5; float cellW = 0.f, cellH = 0.f, gapX = 12.f, gapY = 12.f; SDL_FRect area{0, 0, 0, 0}; SDL_FRect cell(int i) const { int r = i / cols, c = i % cols; return SDL_FRect{ area.x + c * (cellW + gapX), area.y + r * (cellH + gapY), cellW, cellH }; } }; static Grid MakeGrid(const SDL_FRect& panel) { Grid g; float marginX = 34, marginY = 76; g.area = SDL_FRect{ panel.x + marginX, panel.y + marginY, panel.w - 2 * marginX, panel.h - marginY - 28 }; g.cellW = (g.area.w - (g.cols - 1) * g.gapX) / g.cols; g.cellH = (g.area.h - (g.rows - 1) * g.gapY) / g.rows; return g; } static void DrawCell(SDL_Renderer* r, SDL_FRect rc, int idx, bool hovered, bool selected) { FillRect(r, rc, selected ? COL_ACCENT : (hovered ? COL_TILE_H : COL_TILE)); StrokeRect(r, rc, COL_TILE_B); Line(r, rc.x + 2, rc.y + 2, rc.x + rc.w - 2, rc.y + 2, SDL_Color{255, 255, 255, 18}); Line(r, rc.x + 2, rc.y + rc.h - 2, rc.x + rc.w - 2, rc.y + rc.h - 2, SDL_Color{0, 0, 0, 40}); DrawDigit(r, idx, rc.x + rc.w / 2.f, rc.y + rc.h / 2.f); } static int HitTest(const Grid& g, int mx, int my) { for (int i = 0; i < 20; i++) { SDL_FRect rc = g.cell(i); if (mx >= rc.x && mx < rc.x + rc.w && my >= rc.y && my < rc.y + rc.h) return i; } return -1; } 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 (clamped within grid like the sample) int c = hoveredLevel < 0 ? 0 : hoveredLevel; switch (e.key.scancode) { case SDL_SCANCODE_LEFT: if (c % 4 > 0) c--; break; case SDL_SCANCODE_RIGHT: if (c % 4 < 3) c++; break; case SDL_SCANCODE_UP: if (c / 4 > 0) c -= 4; break; case SDL_SCANCODE_DOWN: if (c / 4 < 4) c += 4; break; case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: selectLevel(hoveredLevel < 0 ? 0 : hoveredLevel); return; case SDL_SCANCODE_ESCAPE: closePopup(); return; default: break; } hoveredLevel = c; if (ctx.startLevelSelection) *ctx.startLevelSelection = hoveredLevel; } else if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { if (e.button.button == SDL_BUTTON_LEFT) { // Get dynamic logical dimensions const int LOGICAL_W = GlobalState::instance().getLogicalWidth(); const int LOGICAL_H = GlobalState::instance().getLogicalHeight(); // convert mouse to logical coords (viewport is already centered) float lx = (float(e.button.x) - float(lastLogicalVP.x)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); float ly = (float(e.button.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); // Use same panel calculation as render (centered) const float LOGICAL_W_F = 1200.f; const float LOGICAL_H_F = 1000.f; float winW = (float)lastLogicalVP.w; float winH = (float)lastLogicalVP.h; float contentOffsetX = 0.0f; float contentOffsetY = 0.0f; UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY); float PW = std::min(520.f, LOGICAL_W_F * 0.65f); float PH = std::min(360.f, LOGICAL_H_F * 0.7f); SDL_FRect panel{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH }; Grid g = MakeGrid(panel); int hit = HitTest(g, int(lx), int(ly)); if (hit != -1) { selectLevel(hit); } else { closePopup(); } } } else if (e.type == SDL_EVENT_MOUSE_MOTION) { // Get dynamic logical dimensions const int LOGICAL_W = GlobalState::instance().getLogicalWidth(); const int LOGICAL_H = GlobalState::instance().getLogicalHeight(); // convert mouse to logical coords (viewport is already centered) float lx = (float(e.motion.x) - float(lastLogicalVP.x)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); float ly = (float(e.motion.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); // Use same panel calculation as render (centered) const float LOGICAL_W_F = 1200.f; const float LOGICAL_H_F = 1000.f; float winW = (float)lastLogicalVP.w; float winH = (float)lastLogicalVP.h; float contentOffsetX = 0.0f; float contentOffsetY = 0.0f; UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY); float PW = std::min(520.f, LOGICAL_W_F * 0.65f); float PH = std::min(360.f, LOGICAL_H_F * 0.7f); SDL_FRect panel{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH }; Grid g = MakeGrid(panel); hoveredLevel = HitTest(g, int(lx), int(ly)); } } 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) { // Cache for input conversion lastLogicalScale = logicalScale; lastLogicalVP = logicalVP; drawLevelSelectionPopup(renderer, logicalScale, logicalVP); } void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { if (!renderer) return; // Use fixed logical dimensions to match main.cpp and ensure consistent layout const float LOGICAL_W = 1200.f; const float LOGICAL_H = 1000.f; // Compute content offsets (same approach as MenuState for proper centering) float winW = (float)logicalVP.w; float winH = (float)logicalVP.h; float contentOffsetX = 0.0f; float contentOffsetY = 0.0f; UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY); // Draw the logo at the top (same as MenuState) SDL_Texture* logoToUse = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex; int logoW = 0, logoH = 0; if (logoToUse == ctx.logoSmallTex && ctx.logoSmallW > 0) { logoW = ctx.logoSmallW; logoH = ctx.logoSmallH; } UIRenderer::drawLogo(renderer, logoToUse, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY, logoW, logoH); // Panel and title strip (in logical space) - centered properly with offsets float PW = std::min(520.f, LOGICAL_W * 0.65f); float PH = std::min(360.f, LOGICAL_H * 0.7f); SDL_FRect panel{ (LOGICAL_W - PW) / 2.f + contentOffsetX, (LOGICAL_H - PH) / 2.f - 40.f + contentOffsetY, PW, PH }; UIRenderer::drawSciFiPanel(renderer, panel); // Inner face (LevelSelector specific) SDL_FRect inner{panel.x + 12, panel.y + 56, panel.w - 24, panel.h - 68}; FillRect(renderer, inner, COL_PANEL_IN); StrokeRect(renderer, inner, SDL_Color{24, 31, 41, 180}); // Title text - prefer pixelFont for a blocky title if available, fallback to regular font FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; DrawText(renderer, titleFont, "SELECT STARTING LEVEL", LOGICAL_W / 2.f + contentOffsetX, panel.y + 20.f, 1.2f, COL_TITLE, true, true); // Grid of levels Grid g = MakeGrid(panel); int selectedLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0; for (int i = 0; i < 20; i++) { SDL_FRect rc = g.cell(i); DrawCell(renderer, rc, i, hoveredLevel == i, selectedLevel == i); } // Footer/instructions - use regular TTF font for readability, centered and lower FontAtlas* footerFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; DrawText(renderer, footerFont, "CLICK A LEVEL TO SELECT • ESC = CANCEL", LOGICAL_W / 2.f + contentOffsetX, panel.y + panel.h + 60.f, 1.0f, COL_FOOTER, true, true); } bool LevelSelectorState::isMouseInPopup(float mouseX, float mouseY, float& popupX, float& popupY, float& popupW, float& popupH) { // Get dynamic logical dimensions const int LOGICAL_W = GlobalState::instance().getLogicalWidth(); const int LOGICAL_H = GlobalState::instance().getLogicalHeight(); // Simplified: viewport is already centered, just convert mouse to logical coords (void)mouseX; (void)mouseY; float lx = 0.f, ly = 0.f; if (lastLogicalScale > 0.0f) { lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale; ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale; } const float LOGICAL_W_F = 1200.f; const float LOGICAL_H_F = 1000.f; float winW = (float)lastLogicalVP.w; float winH = (float)lastLogicalVP.h; float contentOffsetX = 0.0f; float contentOffsetY = 0.0f; UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY); float PW = std::min(520.f, LOGICAL_W_F * 0.65f); float PH = std::min(360.f, LOGICAL_H_F * 0.7f); SDL_FRect p{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH }; popupX = p.x; popupY = p.y; popupW = p.w; popupH = p.h; return lx >= popupX && lx <= popupX + popupW && ly >= popupY && ly <= popupY + popupH; } int LevelSelectorState::getLevelFromMouse(float mouseX, float mouseY, float popupX, float popupY, float popupW, float popupH) { // Get dynamic logical dimensions const int LOGICAL_W = GlobalState::instance().getLogicalWidth(); const int LOGICAL_H = GlobalState::instance().getLogicalHeight(); (void)popupX; (void)popupY; (void)popupW; (void)popupH; float lx = 0.f, ly = 0.f; if (lastLogicalScale > 0.0f) { lx = (float(mouseX) - float(lastLogicalVP.x)) / lastLogicalScale; ly = (float(mouseY) - float(lastLogicalVP.y)) / lastLogicalScale; } const float LOGICAL_W_F = 1200.f; const float LOGICAL_H_F = 1000.f; float winW = (float)lastLogicalVP.w; float winH = (float)lastLogicalVP.h; float contentOffsetX = 0.0f; float contentOffsetY = 0.0f; UIRenderer::computeContentOffsets(winW, winH, LOGICAL_W_F, LOGICAL_H_F, lastLogicalScale, contentOffsetX, contentOffsetY); float PW = std::min(520.f, LOGICAL_W_F * 0.65f); float PH = std::min(360.f, LOGICAL_H_F * 0.7f); SDL_FRect p{ (LOGICAL_W_F - PW) / 2.f + contentOffsetX, (LOGICAL_H_F - PH) / 2.f - 40.f + contentOffsetY, PW, PH }; Grid g = MakeGrid(p); return HitTest(g, (int)lx, (int)ly); } void LevelSelectorState::updateHoverFromMouse(float mouseX, float mouseY) { hoveredLevel = getLevelFromMouse(mouseX, mouseY, 0, 0, 0, 0); } void LevelSelectorState::selectLevel(int level) { if (ctx.startLevelSelection) { *ctx.startLevelSelection = level; } // Transition back to menu if (ctx.requestFadeTransition) { ctx.requestFadeTransition(AppState::Menu); } else if (ctx.stateManager) { ctx.stateManager->setState(AppState::Menu); } } void LevelSelectorState::closePopup() { // Transition back to menu without changing level if (ctx.requestFadeTransition) { ctx.requestFadeTransition(AppState::Menu); } else if (ctx.stateManager) { ctx.stateManager->setState(AppState::Menu); } }