new main screen
This commit is contained in:
63
src/main.cpp
63
src/main.cpp
@ -624,7 +624,7 @@ int main(int, char **)
|
||||
// Load the new main screen overlay that sits above the background but below buttons
|
||||
int mainScreenW = 0;
|
||||
int mainScreenH = 0;
|
||||
SDL_Texture* mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen_003.png", &mainScreenW, &mainScreenH);
|
||||
SDL_Texture* mainScreenTex = loadTextureFromImage(renderer, "assets/images/main_screen_004.png", &mainScreenW, &mainScreenH);
|
||||
if (mainScreenTex) {
|
||||
SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded main_screen overlay %dx%d (tex=%p)", mainScreenW, mainScreenH, (void*)mainScreenTex);
|
||||
@ -1574,28 +1574,8 @@ int main(int, char **)
|
||||
} else if (state == AppState::Menu) {
|
||||
// Space flyover backdrop for the main screen
|
||||
spaceWarp.draw(renderer, 1.0f);
|
||||
|
||||
if (mainScreenTex) {
|
||||
float texW = mainScreenW > 0 ? static_cast<float>(mainScreenW) : 0.0f;
|
||||
float texH = mainScreenH > 0 ? static_cast<float>(mainScreenH) : 0.0f;
|
||||
if (texW <= 0.0f || texH <= 0.0f) {
|
||||
if (!SDL_GetTextureSize(mainScreenTex, &texW, &texH)) {
|
||||
texW = texH = 0.0f;
|
||||
}
|
||||
}
|
||||
if (texW > 0.0f && texH > 0.0f) {
|
||||
const float drawH = static_cast<float>(winH);
|
||||
const float scale = drawH / texH;
|
||||
const float drawW = texW * scale;
|
||||
SDL_FRect dst{
|
||||
(winW - drawW) * 0.5f,
|
||||
0.0f,
|
||||
drawW,
|
||||
drawH
|
||||
};
|
||||
SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst);
|
||||
}
|
||||
}
|
||||
// `mainScreenTex` is rendered as a top layer just before presenting
|
||||
// so we don't draw it here. Keep the space warp background only.
|
||||
} else if (state == AppState::LevelSelector || state == AppState::Options) {
|
||||
if (backgroundTex) {
|
||||
SDL_FRect fullRect = { 0, 0, (float)winW, (float)winH };
|
||||
@ -1927,6 +1907,43 @@ int main(int, char **)
|
||||
HelpOverlay::Render(renderer, pixelFont, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY);
|
||||
}
|
||||
|
||||
// Top-layer overlay: render `mainScreenTex` above all other layers when in Menu
|
||||
if (state == AppState::Menu && mainScreenTex) {
|
||||
SDL_SetRenderViewport(renderer, nullptr);
|
||||
SDL_SetRenderScale(renderer, 1.f, 1.f);
|
||||
float texW = mainScreenW > 0 ? static_cast<float>(mainScreenW) : 0.0f;
|
||||
float texH = mainScreenH > 0 ? static_cast<float>(mainScreenH) : 0.0f;
|
||||
if (texW <= 0.0f || texH <= 0.0f) {
|
||||
float iwf = 0.0f, ihf = 0.0f;
|
||||
if (SDL_GetTextureSize(mainScreenTex, &iwf, &ihf) != 0) {
|
||||
iwf = ihf = 0.0f;
|
||||
}
|
||||
texW = iwf;
|
||||
texH = ihf;
|
||||
}
|
||||
if (texW > 0.0f && texH > 0.0f) {
|
||||
const float drawH = static_cast<float>(winH);
|
||||
const float scale = drawH / texH;
|
||||
const float drawW = texW * scale;
|
||||
SDL_FRect dst{
|
||||
(winW - drawW) * 0.5f,
|
||||
0.0f,
|
||||
drawW,
|
||||
drawH
|
||||
};
|
||||
SDL_SetTextureBlendMode(mainScreenTex, SDL_BLENDMODE_BLEND);
|
||||
SDL_RenderTexture(renderer, mainScreenTex, nullptr, &dst);
|
||||
}
|
||||
// Restore logical viewport/scale and draw the main PLAY button above the overlay
|
||||
SDL_SetRenderViewport(renderer, &logicalVP);
|
||||
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
|
||||
if (menuState) {
|
||||
menuState->drawMainButtonNormally = false; // ensure it isn't double-drawn
|
||||
menuState->renderMainButtonTop(renderer, logicalScale, logicalVP);
|
||||
menuState->drawMainButtonNormally = true;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_RenderPresent(renderer);
|
||||
SDL_SetRenderScale(renderer, 1.f, 1.f);
|
||||
}
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -10,6 +10,11 @@ public:
|
||||
void handleEvent(const SDL_Event& e) override;
|
||||
void update(double frameMs) override;
|
||||
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
|
||||
// When false, the main PLAY button is not drawn by `render()` and can be
|
||||
// rendered separately with `renderMainButtonTop` (useful for layer ordering).
|
||||
bool drawMainButtonNormally = true;
|
||||
// Draw only the main PLAY button on top of other layers (expects logical coords).
|
||||
void renderMainButtonTop(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP);
|
||||
|
||||
private:
|
||||
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = EXIT
|
||||
@ -19,4 +24,33 @@ private:
|
||||
SDL_Texture* levelIcon = nullptr;
|
||||
SDL_Texture* optionsIcon = nullptr;
|
||||
SDL_Texture* exitIcon = nullptr;
|
||||
|
||||
// Options panel animation state
|
||||
bool optionsVisible = false;
|
||||
bool optionsAnimating = false;
|
||||
double optionsTransition = 0.0; // 0..1
|
||||
double optionsTransitionDurationMs = 400.0;
|
||||
int optionsDirection = 1; // 1 show, -1 hide
|
||||
// Which row in the inline options panel is currently selected (0..4)
|
||||
int optionsSelectedRow = 0;
|
||||
// Inline level selector HUD state
|
||||
bool levelPanelVisible = false;
|
||||
bool levelPanelAnimating = false;
|
||||
double levelTransition = 0.0; // 0..1
|
||||
double levelTransitionDurationMs = 400.0;
|
||||
int levelDirection = 1; // 1 show, -1 hide
|
||||
int levelHovered = -1; // hovered cell
|
||||
int levelSelected = 0; // current selected level
|
||||
// Cache logical viewport/scale for input conversion when needed
|
||||
float lastLogicalScale = 1.0f;
|
||||
SDL_Rect lastLogicalVP{0,0,0,0};
|
||||
// Animated highlight position (world/logical coordinates)
|
||||
double levelHighlightX = 0.0;
|
||||
double levelHighlightY = 0.0;
|
||||
bool levelHighlightInitialized = false;
|
||||
// Highlight tuning parameters
|
||||
double levelHighlightSpeed = 0.018; // smoothing constant - higher = snappier
|
||||
double levelHighlightGlowAlpha = 0.70; // 0..1 base glow alpha
|
||||
int levelHighlightThickness = 3; // inner outline thickness (px)
|
||||
SDL_Color levelHighlightColor = SDL_Color{80, 200, 255, 200};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user