Files
spacetris/src/states/MenuState.cpp
2025-12-06 12:42:29 +01:00

793 lines
35 KiB
C++

#include "MenuState.h"
#include "persistence/Scores.h"
#include "graphics/Font.h"
#include "../core/GlobalState.h"
#include "../core/Settings.h"
#include "../core/state/StateManager.h"
#include "../audio/Audio.h"
#include "../audio/SoundEffect.h"
#include <SDL3/SDL.h>
#include <cstdio>
#include <algorithm>
#include <array>
#include <cmath>
#include <vector>
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
// This allows the UI to adapt when the window is resized or goes fullscreen
// Shared flags and resources are provided via StateContext `ctx`.
// Removed fragile extern declarations and use `ctx.showLevelPopup`, `ctx.showSettingsPopup`,
// `ctx.musicEnabled` and `ctx.hoveredButton` instead to avoid globals.
// Menu helper wrappers are declared in a shared header implemented in main.cpp
#include "../audio/MenuWrappers.h"
#include "../utils/ImagePathResolver.h"
#include "../graphics/renderers/UIRenderer.h"
#include "../graphics/renderers/GameRenderer.h"
#include <SDL3_image/SDL_image.h>
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
void MenuState::onEnter() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called");
if (ctx.showExitConfirmPopup) {
*ctx.showExitConfirmPopup = false;
}
if (ctx.exitPopupSelectedButton) {
*ctx.exitPopupSelectedButton = 1;
}
}
void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
const float LOGICAL_W = 1200.f;
const float LOGICAL_H = 1000.f;
float contentOffsetX = 0.0f;
float contentOffsetY = 0.0f;
UIRenderer::computeContentOffsets((float)logicalVP.w, (float)logicalVP.h, LOGICAL_W, LOGICAL_H, logicalScale, contentOffsetX, contentOffsetY);
float contentW = LOGICAL_W * logicalScale;
bool isSmall = (contentW < 700.0f);
float btnW = 200.0f;
float btnH = 70.0f;
float btnX = LOGICAL_W * 0.5f + contentOffsetX;
float btnY = LOGICAL_H * 0.865f + contentOffsetY;
// Compose same button definition used in render()
char levelBtnText[32];
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
struct MenuButtonDef { SDL_Color bg; SDL_Color border; std::string label; };
std::array<MenuButtonDef,4> buttons = {
MenuButtonDef{ SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}, "PLAY" },
MenuButtonDef{ SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}, levelBtnText },
MenuButtonDef{ SDL_Color{130,80,210,255}, SDL_Color{90,40,170,255}, "OPTIONS" },
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
};
std::array<SDL_Texture*,4> icons = { playIcon, levelIcon, optionsIcon, exitIcon };
float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f;
// Draw all four buttons on top
for (int i = 0; i < 4; ++i) {
float cxCenter = 0.0f;
float cyCenter = btnY;
if (ctx.menuButtonsExplicit) {
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
} else {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
// Apply small per-button offsets that match the original placements
float extra = 0.0f;
if (i == 0) extra = 15.0f;
if (i == 2) extra = -24.0f;
if (i == 3) extra = -44.0f;
cxCenter = btnX + offset + extra;
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
}
void MenuState::onExit() {
if (ctx.showExitConfirmPopup) {
*ctx.showExitConfirmPopup = false;
}
// Clean up icon textures
if (playIcon) { SDL_DestroyTexture(playIcon); playIcon = nullptr; }
if (levelIcon) { SDL_DestroyTexture(levelIcon); levelIcon = nullptr; }
if (optionsIcon) { SDL_DestroyTexture(optionsIcon); optionsIcon = nullptr; }
if (exitIcon) { SDL_DestroyTexture(exitIcon); exitIcon = nullptr; }
}
void MenuState::handleEvent(const SDL_Event& e) {
// Keyboard navigation for menu buttons
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
auto triggerPlay = [&]() {
if (ctx.startPlayTransition) {
ctx.startPlayTransition();
} else if (ctx.stateManager) {
ctx.stateManager->setState(AppState::Playing);
}
};
auto setExitSelection = [&](int value) {
if (ctx.exitPopupSelectedButton) {
*ctx.exitPopupSelectedButton = value;
}
};
auto getExitSelection = [&]() -> int {
return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
};
auto isExitPromptVisible = [&]() -> bool {
return ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup;
};
auto setExitPrompt = [&](bool visible) {
if (ctx.showExitConfirmPopup) {
*ctx.showExitConfirmPopup = visible;
}
};
if (isExitPromptVisible()) {
switch (e.key.scancode) {
case SDL_SCANCODE_LEFT:
case SDL_SCANCODE_UP:
setExitSelection(0);
return;
case SDL_SCANCODE_RIGHT:
case SDL_SCANCODE_DOWN:
setExitSelection(1);
return;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
case SDL_SCANCODE_SPACE:
if (getExitSelection() == 0) {
setExitPrompt(false);
if (ctx.requestQuit) {
ctx.requestQuit();
} else {
SDL_Event quit{};
quit.type = SDL_EVENT_QUIT;
SDL_PushEvent(&quit);
}
} else {
setExitPrompt(false);
}
return;
case SDL_SCANCODE_ESCAPE:
setExitPrompt(false);
setExitSelection(1);
return;
default:
return;
}
}
// If the inline options HUD is visible and not animating, capture navigation
if (optionsVisible && !optionsAnimating) {
switch (e.key.scancode) {
case SDL_SCANCODE_UP:
{
optionsSelectedRow = (optionsSelectedRow + 5 - 1) % 5;
return;
}
case SDL_SCANCODE_DOWN:
{
optionsSelectedRow = (optionsSelectedRow + 1) % 5;
return;
}
case SDL_SCANCODE_LEFT:
case SDL_SCANCODE_RIGHT:
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
case SDL_SCANCODE_SPACE:
{
// Perform toggle/action for the selected option
switch (optionsSelectedRow) {
case 0: {
// FULLSCREEN
bool nextState = ! (ctx.fullscreenFlag ? *ctx.fullscreenFlag : Settings::instance().isFullscreen());
if (ctx.fullscreenFlag) *ctx.fullscreenFlag = nextState;
if (ctx.applyFullscreen) ctx.applyFullscreen(nextState);
Settings::instance().setFullscreen(nextState);
Settings::instance().save();
return;
}
case 1: {
// MUSIC
bool next = !Settings::instance().isMusicEnabled();
Settings::instance().setMusicEnabled(next);
Settings::instance().save();
if (ctx.musicEnabled) *ctx.musicEnabled = next;
return;
}
case 2: {
// SOUND FX
bool next = !SoundEffectManager::instance().isEnabled();
SoundEffectManager::instance().setEnabled(next);
Settings::instance().setSoundEnabled(next);
Settings::instance().save();
return;
}
case 3: {
// SMOOTH SCROLL
bool next = !Settings::instance().isSmoothScrollEnabled();
Settings::instance().setSmoothScrollEnabled(next);
Settings::instance().save();
return;
}
case 4: {
// RETURN TO MENU -> hide panel
optionsAnimating = true;
optionsDirection = -1;
return;
}
}
}
default:
break;
}
}
// If inline level HUD visible and not animating, capture navigation
if (levelPanelVisible && !levelPanelAnimating) {
int c = levelSelected < 0 ? 0 : levelSelected;
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:
case SDL_SCANCODE_SPACE:
levelSelected = c;
if (ctx.startLevelSelection) *ctx.startLevelSelection = levelSelected;
// close HUD
levelPanelAnimating = true; levelDirection = -1;
return;
case SDL_SCANCODE_ESCAPE:
levelPanelAnimating = true; levelDirection = -1;
return;
default: break;
}
levelSelected = c;
if (ctx.startLevelSelection) *ctx.startLevelSelection = levelSelected;
// Consume the event so main menu navigation does not also run
return;
}
switch (e.key.scancode) {
case SDL_SCANCODE_LEFT:
case SDL_SCANCODE_UP:
{
const int total = 4;
selectedButton = (selectedButton + total - 1) % total;
break;
}
case SDL_SCANCODE_RIGHT:
case SDL_SCANCODE_DOWN:
{
const int total = 4;
selectedButton = (selectedButton + 1) % total;
break;
}
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
case SDL_SCANCODE_SPACE:
if (!ctx.stateManager) {
break;
}
switch (selectedButton) {
case 0:
triggerPlay();
break;
case 1:
// Toggle inline level selector HUD (show/hide)
if (!levelPanelVisible && !levelPanelAnimating) {
levelPanelAnimating = true;
levelDirection = 1; // show
} else if (levelPanelVisible && !levelPanelAnimating) {
levelPanelAnimating = true;
levelDirection = -1; // hide
}
break;
case 2:
// Toggle the options panel with an animated slide-in/out.
if (!optionsVisible && !optionsAnimating) {
optionsAnimating = true;
optionsDirection = 1; // show
} else if (optionsVisible && !optionsAnimating) {
optionsAnimating = true;
optionsDirection = -1; // hide
}
break;
case 3:
setExitPrompt(true);
setExitSelection(1);
break;
}
break;
case SDL_SCANCODE_ESCAPE:
// If options panel is visible, hide it first.
if (optionsVisible && !optionsAnimating) {
optionsAnimating = true;
optionsDirection = -1;
return;
}
setExitPrompt(true);
setExitSelection(1);
break;
default:
break;
}
}
}
void MenuState::update(double frameMs) {
// Update logo animation counter
GlobalState::instance().logoAnimCounter += frameMs;
// Advance options panel animation if active
if (optionsAnimating) {
double delta = (frameMs / optionsTransitionDurationMs) * static_cast<double>(optionsDirection);
optionsTransition += delta;
if (optionsTransition >= 1.0) {
optionsTransition = 1.0;
optionsVisible = true;
optionsAnimating = false;
} else if (optionsTransition <= 0.0) {
optionsTransition = 0.0;
optionsVisible = false;
optionsAnimating = false;
}
}
// Advance level panel animation if active
if (levelPanelAnimating) {
double delta = (frameMs / levelTransitionDurationMs) * static_cast<double>(levelDirection);
levelTransition += delta;
if (levelTransition >= 1.0) {
levelTransition = 1.0;
levelPanelVisible = true;
levelPanelAnimating = false;
} else if (levelTransition <= 0.0) {
levelTransition = 0.0;
levelPanelVisible = false;
levelPanelAnimating = false;
}
}
// Animate level selection highlight position toward the selected cell center
if (levelTransition > 0.0 && (lastLogicalScale > 0.0f)) {
// Recompute same grid geometry used in render to find target center
const float LOGICAL_W = 1200.f;
const float LOGICAL_H = 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, LOGICAL_H, lastLogicalScale, contentOffsetX, contentOffsetY);
const float PW = std::min(520.0f, LOGICAL_W * 0.65f);
const float PH = std::min(360.0f, LOGICAL_H * 0.7f);
float panelBaseX = (LOGICAL_W - PW) * 0.5f + contentOffsetX;
float panelBaseY = (LOGICAL_H - PH) * 0.5f + contentOffsetY - (LOGICAL_H * 0.10f);
float marginX = 34.0f, marginY = 56.0f;
SDL_FRect area{ panelBaseX + marginX, panelBaseY + marginY, PW - 2.0f * marginX, PH - marginY - 28.0f };
const int cols = 4, rows = 5;
const float gapX = 12.0f, gapY = 12.0f;
float cellW = (area.w - (cols - 1) * gapX) / cols;
float cellH = (area.h - (rows - 1) * gapY) / rows;
int targetIdx = std::clamp(levelSelected, 0, 19);
int tr = targetIdx / cols, tc = targetIdx % cols;
double targetX = area.x + tc * (cellW + gapX) + cellW * 0.5f;
double targetY = area.y + tr * (cellH + gapY) + cellH * 0.5f;
if (!levelHighlightInitialized) {
levelHighlightX = targetX;
levelHighlightY = targetY;
levelHighlightInitialized = true;
} else {
// Exponential smoothing: alpha = 1 - exp(-k * dt)
double k = levelHighlightSpeed; // user-tunable speed constant
double alpha = 1.0 - std::exp(-k * frameMs);
if (alpha < 1e-6) alpha = std::min(1.0, frameMs * 0.02);
levelHighlightX += (targetX - levelHighlightX) * alpha;
levelHighlightY += (targetY - levelHighlightY) * alpha;
}
}
}
void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
// Use fixed logical dimensions to match main.cpp and ensure consistent layout
// This prevents the UI from floating apart on wide/tall screens
const float LOGICAL_W = 1200.f;
const float LOGICAL_H = 1000.f;
// Trace entry to persistent log for debugging abrupt exit/crash during render
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render entry\n"); fclose(f); }
}
// Compute content offsets (same approach as main.cpp 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);
// Cache logical viewport/scale for update() so it can compute HUD target positions
lastLogicalScale = logicalScale;
lastLogicalVP = logicalVP;
// Background is drawn by main (stretched to the full window) to avoid double-draw.
{
FILE* f = fopen("tetris_trace.log", "a");
if (f) {
fprintf(f, "MenuState::render ctx.mainScreenTex=%llu (w=%d h=%d)\n",
(unsigned long long)(uintptr_t)ctx.mainScreenTex,
ctx.mainScreenW,
ctx.mainScreenH);
fclose(f);
}
}
FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
// Slide-space amount for the options HUD (how much highscores move)
const float moveAmount = 420.0f; // increased so lower score rows slide further up
// Compute eased transition and delta to shift highscores when either options or level HUD is shown.
float combinedTransition = static_cast<float>(std::max(optionsTransition, levelTransition));
float eased = combinedTransition * combinedTransition * (3.0f - 2.0f * combinedTransition); // cubic smoothstep
float panelDelta = eased * moveAmount;
float topPlayersY = LOGICAL_H * 0.24f + contentOffsetY - panelDelta;
if (useFont) {
const std::string title = "TOP PLAYERS";
int tW = 0, tH = 0;
useFont->measure(title, 1.8f, tW, tH);
float titleX = (LOGICAL_W - (float)tW) * 0.5f + contentOffsetX;
useFont->draw(renderer, titleX, topPlayersY, title, 1.8f, SDL_Color{255, 220, 0, 255});
}
float scoresStartY = topPlayersY + 70.0f;
static const std::vector<ScoreEntry> EMPTY_SCORES;
const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES;
size_t maxDisplay = std::min(hs.size(), size_t(12));
if (useFont) {
float cx = LOGICAL_W * 0.5f + contentOffsetX;
float colX[] = { cx - 280, cx - 180, cx - 20, cx + 90, cx + 200, cx + 300 };
useFont->draw(renderer, colX[0], scoresStartY - 28, "RANK", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[1], scoresStartY - 28, "PLAYER", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[2], scoresStartY - 28, "SCORE", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[3], scoresStartY - 28, "LINES", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[4], scoresStartY - 28, "LEVEL", 1.1f, SDL_Color{200,200,220,255});
useFont->draw(renderer, colX[5], scoresStartY - 28, "TIME", 1.1f, SDL_Color{200,200,220,255});
for (size_t i = 0; i < maxDisplay; ++i) {
float baseY = scoresStartY + i * 25.0f - panelDelta;
float wave = std::sin((float)GlobalState::instance().logoAnimCounter * 0.006f + i * 0.25f) * 6.0f;
float y = baseY + wave;
char rankStr[8];
std::snprintf(rankStr, sizeof(rankStr), "%zu.", i + 1);
useFont->draw(renderer, colX[0], y, rankStr, 1.0f, SDL_Color{220, 220, 230, 255});
useFont->draw(renderer, colX[1], y, hs[i].name, 1.0f, SDL_Color{220, 220, 230, 255});
char scoreStr[16];
std::snprintf(scoreStr, sizeof(scoreStr), "%d", hs[i].score);
useFont->draw(renderer, colX[2], y, scoreStr, 1.0f, SDL_Color{220, 220, 230, 255});
char linesStr[8];
std::snprintf(linesStr, sizeof(linesStr), "%d", hs[i].lines);
useFont->draw(renderer, colX[3], y, linesStr, 1.0f, SDL_Color{220, 220, 230, 255});
char levelStr[8];
std::snprintf(levelStr, sizeof(levelStr), "%d", hs[i].level);
useFont->draw(renderer, colX[4], y, levelStr, 1.0f, SDL_Color{220, 220, 230, 255});
char timeStr[16];
int mins = int(hs[i].timeSec) / 60;
int secs = int(hs[i].timeSec) % 60;
std::snprintf(timeStr, sizeof(timeStr), "%d:%02d", mins, secs);
useFont->draw(renderer, colX[5], y, timeStr, 1.0f, SDL_Color{220, 220, 230, 255});
}
}
// The main_screen overlay is drawn by main.cpp as the background
// We don't need to draw it again here as a logo
// Draw bottom action buttons with responsive sizing (reduced to match main mouse hit-test)
// Use the contentW calculated at the top with content offsets
float contentW = LOGICAL_W * logicalScale;
bool isSmall = (contentW < 700.0f);
// Adjust button dimensions to match the background button graphics
float btnW = 200.0f; // Fixed width to match background buttons
float btnH = 70.0f; // Fixed height to match background buttons
float btnX = LOGICAL_W * 0.5f + contentOffsetX;
// Adjust vertical position to align with background buttons
float btnY = LOGICAL_H * 0.865f + contentOffsetY;
if (ctx.pixelFont) {
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render drawing buttons; pixelFont=%llu\n", (unsigned long long)(uintptr_t)ctx.pixelFont); fclose(f); }
}
char levelBtnText[32];
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
struct MenuButtonDef {
SDL_Color bg;
SDL_Color border;
std::string label;
};
std::array<MenuButtonDef, 4> buttons = {
MenuButtonDef{ SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}, "PLAY" },
MenuButtonDef{ SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}, levelBtnText },
MenuButtonDef{ SDL_Color{130,80,210,255}, SDL_Color{90,40,170,255}, "OPTIONS" },
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
};
// Icon array (nullptr if icon not loaded)
std::array<SDL_Texture*, 4> icons = {
playIcon,
levelIcon,
optionsIcon,
exitIcon
};
// Fixed spacing to match background button positions
float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f;
// Draw each button individually so each can have its own coordinates
if (drawMainButtonNormally) {
// Button 0 - PLAY
{
const int i = 0;
float cxCenter = 0.0f;
float cyCenter = btnY;
if (ctx.menuButtonsExplicit) {
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
} else {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
cxCenter = btnX + offset + 15.0f;
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
// Button 1 - LEVEL
{
const int i = 1;
float cxCenter = 0.0f;
float cyCenter = btnY;
if (ctx.menuButtonsExplicit) {
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
} else {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
cxCenter = btnX + offset;
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
// Button 2 - OPTIONS
{
const int i = 2;
float cxCenter = 0.0f;
float cyCenter = btnY;
if (ctx.menuButtonsExplicit) {
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
} else {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
cxCenter = btnX + offset - 24.0f;
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
// Button 3 - EXIT
{
const int i = 3;
float cxCenter = 0.0f;
float cyCenter = btnY;
if (ctx.menuButtonsExplicit) {
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
} else {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
cxCenter = btnX + offset - 44.0f;
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
}
}
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
GameRenderer::renderExitPopup(
renderer,
ctx.pixelFont,
winW,
winH,
logicalScale,
ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1
);
}
// Popups (settings only - level popup is now a separate state)
if (ctx.showSettingsPopup && *ctx.showSettingsPopup) {
bool musicOn = ctx.musicEnabled ? *ctx.musicEnabled : true;
bool soundOn = SoundEffectManager::instance().isEnabled();
UIRenderer::drawSettingsPopup(renderer, ctx.font, LOGICAL_W, LOGICAL_H, musicOn, soundOn);
}
// Draw animated options panel if in use (either animating or visible)
if (optionsTransition > 0.0) {
// HUD-style overlay: no opaque background; draw labels/values directly with separators
const float panelW = 520.0f;
const float panelH = 420.0f;
float panelBaseX = (LOGICAL_W - panelW) * 0.5f + contentOffsetX;
// Move the HUD higher by ~10% of logical height so it sits above center
float panelBaseY = (LOGICAL_H - panelH) * 0.5f + contentOffsetY - (LOGICAL_H * 0.10f);
float panelY = panelBaseY + (1.0f - eased) * moveAmount;
// For options/settings we prefer the secondary (Exo2) font for longer descriptions.
FontAtlas* retroFont = ctx.font ? ctx.font : ctx.pixelFont;
if (retroFont) {
retroFont->draw(renderer, panelBaseX + 12.0f, panelY + 6.0f, "OPTIONS", 1.8f, SDL_Color{120, 220, 255, 220});
}
SDL_FRect area{panelBaseX, panelY + 48.0f, panelW, panelH - 64.0f};
constexpr int rowCount = 5;
const float rowHeight = 64.0f;
const float spacing = 8.0f;
auto drawField = [&](int idx, float y, const std::string& label, const std::string& value) {
SDL_FRect row{area.x, y, area.w, rowHeight};
// Draw thin separator (1px high filled rect) so we avoid platform-specific line API differences
SDL_SetRenderDrawColor(renderer, 60, 120, 160, 120);
SDL_FRect sep{ row.x + 6.0f, row.y + row.h - 1.0f, row.w - 12.0f, 1.0f };
SDL_RenderFillRect(renderer, &sep);
// Highlight the selected row with a subtle outline
if (idx == optionsSelectedRow) {
SDL_SetRenderDrawColor(renderer, 80, 200, 255, 120);
SDL_RenderRect(renderer, &row);
}
if (retroFont) {
SDL_Color labelColor = SDL_Color{170, 210, 220, 220};
SDL_Color valueColor = SDL_Color{160, 240, 255, 240};
if (!label.empty()) {
float labelScale = 1.0f;
int labelW = 0, labelH = 0;
retroFont->measure(label, labelScale, labelW, labelH);
float labelY = row.y + (row.h - static_cast<float>(labelH)) * 0.5f;
retroFont->draw(renderer, row.x + 16.0f, labelY, label, labelScale, labelColor);
}
int valueW = 0, valueH = 0;
float valueScale = 1.4f;
retroFont->measure(value, valueScale, valueW, valueH);
float valX = row.x + row.w - static_cast<float>(valueW) - 16.0f;
float valY = row.y + (row.h - valueH) * 0.5f;
retroFont->draw(renderer, valX, valY, value, valueScale, valueColor);
}
};
float rowY = area.y + spacing;
// FULLSCREEN
bool isFS = ctx.fullscreenFlag ? *ctx.fullscreenFlag : Settings::instance().isFullscreen();
drawField(0, rowY, "FULLSCREEN", isFS ? "ON" : "OFF");
rowY += rowHeight + spacing;
// MUSIC
bool musicOn = ctx.musicEnabled ? *ctx.musicEnabled : Settings::instance().isMusicEnabled();
drawField(1, rowY, "MUSIC", musicOn ? "ON" : "OFF");
rowY += rowHeight + spacing;
// SOUND FX
bool soundOn = SoundEffectManager::instance().isEnabled();
drawField(2, rowY, "SOUND FX", soundOn ? "ON" : "OFF");
rowY += rowHeight + spacing;
// SMOOTH SCROLL
bool smooth = Settings::instance().isSmoothScrollEnabled();
drawField(3, rowY, "SMOOTH SCROLL", smooth ? "ON" : "OFF");
rowY += rowHeight + spacing;
// RETURN TO MENU
drawField(4, rowY, "", "RETURN TO MENU");
}
// Draw inline level selector HUD (no background) if active
if (levelTransition > 0.0) {
float easedL = static_cast<float>(levelTransition);
easedL = easedL * easedL * (3.0f - 2.0f * easedL);
const float PW = std::min(520.0f, LOGICAL_W * 0.65f);
const float PH = std::min(360.0f, LOGICAL_H * 0.7f);
float panelBaseX = (LOGICAL_W - PW) * 0.5f + contentOffsetX;
float panelBaseY = (LOGICAL_H - PH) * 0.5f + contentOffsetY - (LOGICAL_H * 0.10f);
float slideAmount = LOGICAL_H * 0.42f;
float panelY = panelBaseY + (1.0f - easedL) * slideAmount;
// Header
FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
if (titleFont) titleFont->draw(renderer, panelBaseX + PW * 0.5f - 140.0f, panelY + 6.0f, "SELECT STARTING LEVEL", 1.1f, SDL_Color{160,220,255,220});
// Grid area
float marginX = 34.0f, marginY = 56.0f;
SDL_FRect area{ panelBaseX + marginX, panelY + marginY, PW - 2.0f * marginX, PH - marginY - 28.0f };
const int cols = 4, rows = 5;
const float gapX = 12.0f, gapY = 12.0f;
float cellW = (area.w - (cols - 1) * gapX) / cols;
float cellH = (area.h - (rows - 1) * gapY) / rows;
for (int i = 0; i < 20; ++i) {
int r = i / cols, c = i % cols;
SDL_FRect rc{ area.x + c * (cellW + gapX), area.y + r * (cellH + gapY), cellW, cellH };
bool hovered = (levelSelected == i) || (levelHovered == i);
bool selected = (ctx.startLevelSelection && *ctx.startLevelSelection == i);
SDL_Color fill = selected ? SDL_Color{255,140,40,160} : (hovered ? SDL_Color{60,80,100,120} : SDL_Color{30,40,60,110});
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, fill.r, fill.g, fill.b, fill.a);
SDL_RenderFillRect(renderer, &rc);
SDL_SetRenderDrawColor(renderer, 80,100,120,160);
SDL_RenderRect(renderer, &rc);
// Draw level number
FontAtlas* f = ctx.font ? ctx.font : ctx.pixelFont;
if (f) {
char buf[8]; std::snprintf(buf, sizeof(buf), "%d", i);
int w=0,h=0; f->measure(buf, 1.6f, w, h);
f->draw(renderer, rc.x + (rc.w - w) * 0.5f, rc.y + (rc.h - h) * 0.5f, buf, 1.6f, SDL_Color{220,230,240,255});
}
}
// Draw animated highlight (interpolated) on top of cells
if (levelHighlightInitialized) {
float hx = (float)levelHighlightX;
float hy = (float)levelHighlightY;
float hw = cellW + 6.0f;
float hh = cellH + 6.0f;
// Draw multi-layer glow: outer faint, mid, inner bright
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Outer glow
SDL_SetRenderDrawColor(renderer, levelHighlightColor.r, levelHighlightColor.g, levelHighlightColor.b, (Uint8)(levelHighlightGlowAlpha * 80));
SDL_FRect outer{ hx - (hw * 0.5f + 8.0f), hy - (hh * 0.5f + 8.0f), hw + 16.0f, hh + 16.0f };
SDL_RenderRect(renderer, &outer);
// Mid glow
SDL_SetRenderDrawColor(renderer, levelHighlightColor.r, levelHighlightColor.g, levelHighlightColor.b, (Uint8)(levelHighlightGlowAlpha * 140));
SDL_FRect mid{ hx - (hw * 0.5f + 4.0f), hy - (hh * 0.5f + 4.0f), hw + 8.0f, hh + 8.0f };
SDL_RenderRect(renderer, &mid);
// Inner outline
SDL_SetRenderDrawColor(renderer, levelHighlightColor.r, levelHighlightColor.g, levelHighlightColor.b, levelHighlightColor.a);
SDL_FRect inner{ hx - hw * 0.5f, hy - hh * 0.5f, hw, hh };
// Draw multiple rects to simulate thickness
for (int t = 0; t < levelHighlightThickness; ++t) {
SDL_FRect r{ inner.x - t, inner.y - t, inner.w + t * 2.0f, inner.h + t * 2.0f };
SDL_RenderRect(renderer, &r);
}
}
// Instructions
FontAtlas* foot = ctx.font ? ctx.font : ctx.pixelFont;
if (foot) foot->draw(renderer, panelBaseX + PW*0.5f - 160.0f, panelY + PH + 40.0f, "ARROWS = NAV • ENTER = SELECT • ESC = CANCEL", 0.9f, SDL_Color{160,180,200,200});
}
// Trace exit
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render exit\n"); fclose(f); }
}
}