793 lines
35 KiB
C++
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); }
|
|
}
|
|
}
|