new main screen

This commit is contained in:
2025-12-06 12:42:29 +01:00
parent fff14fe3e1
commit cb8293175b
4 changed files with 537 additions and 92 deletions

View File

@ -2,6 +2,7 @@
#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"
@ -37,6 +38,59 @@ void MenuState::onEnter() {
}
}
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;
@ -112,6 +166,99 @@ void MenuState::handleEvent(const SDL_Event& e) {
}
}
// 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:
@ -138,17 +285,23 @@ void MenuState::handleEvent(const SDL_Event& e) {
triggerPlay();
break;
case 1:
if (ctx.requestFadeTransition) {
ctx.requestFadeTransition(AppState::LevelSelector);
} else if (ctx.stateManager) {
ctx.stateManager->setState(AppState::LevelSelector);
// 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:
if (ctx.requestFadeTransition) {
ctx.requestFadeTransition(AppState::Options);
} else if (ctx.stateManager) {
ctx.stateManager->setState(AppState::Options);
// 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:
@ -158,6 +311,12 @@ void MenuState::handleEvent(const SDL_Event& e) {
}
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;
@ -170,6 +329,76 @@ void MenuState::handleEvent(const SDL_Event& e) {
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) {
@ -190,6 +419,10 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
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");
@ -203,7 +436,15 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
}
FontAtlas* useFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
float topPlayersY = LOGICAL_H * 0.24f + contentOffsetY;
// 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;
@ -228,7 +469,7 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
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;
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;
@ -305,72 +546,74 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f;
// Draw each button individually so each can have its own coordinates
// 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;
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]);
}
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;
// 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]);
}
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;
// 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]);
}
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;
// 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]);
}
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
}
}
@ -391,6 +634,157 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
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); }