Files
spacetris/src/states/LevelSelectorState.cpp

344 lines
15 KiB
C++

// 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 <SDL3/SDL.h>
#include <algorithm>
#include <cstdio>
#include <string>
// 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});
}
static SDL_FRect DrawPanel(SDL_Renderer* r, float w, float h, bool draw = true, float offX = 0.f, float offY = 0.f) {
float PW = std::min(520.f, w * 0.65f);
float PH = std::min(360.f, h * 0.7f);
SDL_FRect p{ (w - PW) / 2.f + offX, (h - PH) / 2.f - 40.f + offY, PW, PH }; // Moved up by 50px
if (!draw) return p; // geometry only
// drop shadow
FillRect(r, SDL_FRect{p.x + 6, p.y + 10, p.w, p.h}, SDL_Color{0, 0, 0, 120});
// glow aura
for (int i = 0; i < 6; i++) {
SDL_FRect g{ p.x - (float)(i * 2), p.y - (float)(i * 2), p.w + (float)(i * 4), p.h + (float)(i * 4) };
SDL_Color c = COL_CYAN_SO; c.a = (Uint8)(36 - i * 6);
StrokeRect(r, g, c);
}
// outer body + border
FillRect(r, p, COL_PANEL);
StrokeRect(r, p, COL_CYAN);
// inner face
FillRect(r, SDL_FRect{p.x + 12, p.y + 56, p.w - 24, p.h - 68}, COL_PANEL_IN);
StrokeRect(r, SDL_FRect{p.x + 12, p.y + 56, p.w - 24, p.h - 68}, SDL_Color{24, 31, 41, 180});
return p;
}
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)
SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
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)
SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
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 contentW = LOGICAL_W * logicalScale;
float contentH = LOGICAL_H * logicalScale;
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
// Draw the logo at the top (same as MenuState)
SDL_Texture* logoToUse = ctx.logoSmallTex ? ctx.logoSmallTex : ctx.logoTex;
if (logoToUse) {
// Use dimensions provided by the shared context when available
int texW = (logoToUse == ctx.logoSmallTex && ctx.logoSmallW > 0) ? ctx.logoSmallW : 872;
int texH = (logoToUse == ctx.logoSmallTex && ctx.logoSmallH > 0) ? ctx.logoSmallH : 273;
float maxW = LOGICAL_W * 0.6f; // Match MenuState and OptionsState
float scale = std::min(1.0f, maxW / float(texW));
float dw = texW * scale;
float dh = texH * scale;
float logoX = (LOGICAL_W - dw) / 2.f + contentOffsetX;
float logoY = LOGICAL_H * 0.05f + contentOffsetY; // Match MenuState and OptionsState
SDL_FRect dst{logoX, logoY, dw, dh};
SDL_RenderTexture(renderer, logoToUse, nullptr, &dst);
}
// Panel and title strip (in logical space) - centered properly with offsets
SDL_FRect panel = DrawPanel(renderer, LOGICAL_W, LOGICAL_H, /*draw=*/true, contentOffsetX, contentOffsetY);
// 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;
}
SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
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;
}
SDL_FRect p = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f);
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);
}
}