This commit is contained in:
2025-12-06 14:08:24 +01:00
parent cb8293175b
commit b531bbc798
2 changed files with 219 additions and 35 deletions

View File

@ -85,9 +85,15 @@ void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale,
if (i == 3) extra = -44.0f;
cxCenter = btnX + offset + extra;
}
// Apply group alpha and transient flash to button colors
double aMul = std::clamp(buttonGroupAlpha + buttonFlash * buttonFlashAmount, 0.0, 1.0);
SDL_Color bgCol = buttons[i].bg;
SDL_Color bdCol = buttons[i].border;
bgCol.a = static_cast<Uint8>(std::round(aMul * static_cast<double>(bgCol.a)));
bdCol.a = static_cast<Uint8>(std::round(aMul * static_cast<double>(bdCol.a)));
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
buttons[i].label, false, selectedButton == i,
buttons[i].bg, buttons[i].border, true, icons[i]);
bgCol, bdCol, true, icons[i]);
}
}
@ -131,36 +137,49 @@ void MenuState::handleEvent(const SDL_Event& e) {
}
};
if (isExitPromptVisible()) {
// Inline exit HUD handling (replaces the old modal popup)
if (exitPanelVisible && !exitPanelAnimating) {
switch (e.key.scancode) {
case SDL_SCANCODE_LEFT:
case SDL_SCANCODE_UP:
setExitSelection(0);
// Move selection to YES
exitSelectedButton = 0;
if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton;
return;
case SDL_SCANCODE_RIGHT:
case SDL_SCANCODE_DOWN:
setExitSelection(1);
// Move selection to NO
exitSelectedButton = 1;
if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton;
return;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
case SDL_SCANCODE_SPACE:
if (getExitSelection() == 0) {
setExitPrompt(false);
if (exitSelectedButton == 0) {
// Confirm quit
if (ctx.requestQuit) {
ctx.requestQuit();
} else {
SDL_Event quit{};
quit.type = SDL_EVENT_QUIT;
SDL_PushEvent(&quit);
SDL_Event quit{}; quit.type = SDL_EVENT_QUIT; SDL_PushEvent(&quit);
}
} else {
setExitPrompt(false);
// Close HUD
exitPanelAnimating = true; exitDirection = -1;
if (ctx.showExitConfirmPopup) *ctx.showExitConfirmPopup = false;
}
return;
case SDL_SCANCODE_ESCAPE:
setExitPrompt(false);
setExitSelection(1);
// Close HUD
exitPanelAnimating = true; exitDirection = -1;
if (ctx.showExitConfirmPopup) *ctx.showExitConfirmPopup = false;
return;
case SDL_SCANCODE_PAGEDOWN:
case SDL_SCANCODE_DOWN: {
// scroll content down
exitScroll += 40.0; return;
}
case SDL_SCANCODE_PAGEUP:
case SDL_SCANCODE_UP: {
exitScroll -= 40.0; if (exitScroll < 0.0) exitScroll = 0.0; return;
}
default:
return;
}
@ -168,15 +187,17 @@ void MenuState::handleEvent(const SDL_Event& e) {
// If the inline options HUD is visible and not animating, capture navigation
if (optionsVisible && !optionsAnimating) {
// Options now has more rows; use OPTIONS_ROW_COUNT
constexpr int OPTIONS_ROW_COUNT = 8;
switch (e.key.scancode) {
case SDL_SCANCODE_UP:
{
optionsSelectedRow = (optionsSelectedRow + 5 - 1) % 5;
optionsSelectedRow = (optionsSelectedRow + OPTIONS_ROW_COUNT - 1) % OPTIONS_ROW_COUNT;
return;
}
case SDL_SCANCODE_DOWN:
{
optionsSelectedRow = (optionsSelectedRow + 1) % 5;
optionsSelectedRow = (optionsSelectedRow + 1) % OPTIONS_ROW_COUNT;
return;
}
case SDL_SCANCODE_LEFT:
@ -220,6 +241,25 @@ void MenuState::handleEvent(const SDL_Event& e) {
return;
}
case 4: {
// PULSE ENABLE
buttonPulseEnabled = !buttonPulseEnabled;
return;
}
case 5: {
// PULSE SPEED (Enter toggles to default)
if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
buttonPulseSpeed = 1.0;
}
return;
}
case 6: {
// PULSE MIN ALPHA (Enter resets)
if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
buttonPulseMinAlpha = 0.6;
}
return;
}
case 7: {
// RETURN TO MENU -> hide panel
optionsAnimating = true;
optionsDirection = -1;
@ -234,7 +274,8 @@ void MenuState::handleEvent(const SDL_Event& e) {
// If inline level HUD visible and not animating, capture navigation
if (levelPanelVisible && !levelPanelAnimating) {
int c = levelSelected < 0 ? 0 : levelSelected;
// Start navigation from tentative hover if present, otherwise from committed selection
int c = (levelHovered >= 0) ? levelHovered : (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;
@ -243,18 +284,22 @@ void MenuState::handleEvent(const SDL_Event& e) {
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
case SDL_SCANCODE_SPACE:
// Confirm tentative selection
levelSelected = c;
if (ctx.startLevelSelection) *ctx.startLevelSelection = levelSelected;
// close HUD
levelPanelAnimating = true; levelDirection = -1;
// clear hover candidate
levelHovered = -1;
return;
case SDL_SCANCODE_ESCAPE:
levelPanelAnimating = true; levelDirection = -1;
levelHovered = -1;
return;
default: break;
}
levelSelected = c;
if (ctx.startLevelSelection) *ctx.startLevelSelection = levelSelected;
// Move tentative cursor (don't commit to startLevelSelection yet)
levelHovered = c;
// Consume the event so main menu navigation does not also run
return;
}
@ -265,6 +310,8 @@ void MenuState::handleEvent(const SDL_Event& e) {
{
const int total = 4;
selectedButton = (selectedButton + total - 1) % total;
// brief bright flash on navigation
buttonFlash = 1.0;
break;
}
case SDL_SCANCODE_RIGHT:
@ -272,6 +319,8 @@ void MenuState::handleEvent(const SDL_Event& e) {
{
const int total = 4;
selectedButton = (selectedButton + 1) % total;
// brief bright flash on navigation
buttonFlash = 1.0;
break;
}
case SDL_SCANCODE_RETURN:
@ -289,6 +338,8 @@ void MenuState::handleEvent(const SDL_Event& e) {
if (!levelPanelVisible && !levelPanelAnimating) {
levelPanelAnimating = true;
levelDirection = 1; // show
// initialize tentative cursor to current selected level
levelHovered = levelSelected < 0 ? 0 : levelSelected;
} else if (levelPanelVisible && !levelPanelAnimating) {
levelPanelAnimating = true;
levelDirection = -1; // hide
@ -305,8 +356,14 @@ void MenuState::handleEvent(const SDL_Event& e) {
}
break;
case 3:
setExitPrompt(true);
setExitSelection(1);
// Show the inline exit HUD
if (!exitPanelVisible && !exitPanelAnimating) {
exitPanelAnimating = true;
exitDirection = 1;
exitSelectedButton = 1;
if (ctx.showExitConfirmPopup) *ctx.showExitConfirmPopup = true;
if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton;
}
break;
}
break;
@ -317,8 +374,14 @@ void MenuState::handleEvent(const SDL_Event& e) {
optionsDirection = -1;
return;
}
setExitPrompt(true);
setExitSelection(1);
// Show inline exit HUD on ESC
if (!exitPanelVisible && !exitPanelAnimating) {
exitPanelAnimating = true;
exitDirection = 1;
exitSelectedButton = 1;
if (ctx.showExitConfirmPopup) *ctx.showExitConfirmPopup = true;
if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton;
}
break;
default:
break;
@ -359,6 +422,21 @@ void MenuState::update(double frameMs) {
}
}
// Advance exit panel animation if active
if (exitPanelAnimating) {
double delta = (frameMs / exitTransitionDurationMs) * static_cast<double>(exitDirection);
exitTransition += delta;
if (exitTransition >= 1.0) {
exitTransition = 1.0;
exitPanelVisible = true;
exitPanelAnimating = false;
} else if (exitTransition <= 0.0) {
exitTransition = 0.0;
exitPanelVisible = false;
exitPanelAnimating = 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
@ -381,7 +459,7 @@ void MenuState::update(double frameMs) {
float cellW = (area.w - (cols - 1) * gapX) / cols;
float cellH = (area.h - (rows - 1) * gapY) / rows;
int targetIdx = std::clamp(levelSelected, 0, 19);
int targetIdx = std::clamp((levelHovered >= 0 ? levelHovered : 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;
@ -399,6 +477,47 @@ void MenuState::update(double frameMs) {
levelHighlightY += (targetY - levelHighlightY) * alpha;
}
}
// Update button group pulsing animation
if (buttonPulseEnabled) {
buttonPulseTime += frameMs;
double t = (buttonPulseTime * 0.001) * buttonPulseSpeed; // seconds * speed
double s = 0.0;
switch (buttonPulseEasing) {
case 0: // sin
s = (std::sin(t * 2.0 * 3.14159265358979323846) * 0.5) + 0.5;
break;
case 1: // triangle
{
double ft = t - std::floor(t);
s = (ft < 0.5) ? (ft * 2.0) : (2.0 - ft * 2.0);
break;
}
case 2: // exponential ease-in-out (normalized)
{
double ft = t - std::floor(t);
if (ft < 0.5) {
s = 0.5 * std::pow(2.0, 20.0 * ft - 10.0);
} else {
s = 1.0 - 0.5 * std::pow(2.0, -20.0 * ft + 10.0);
}
// Clamp to 0..1 in case of numeric issues
if (s < 0.0) s = 0.0; if (s > 1.0) s = 1.0;
break;
}
default:
s = (std::sin(t * 2.0 * 3.14159265358979323846) * 0.5) + 0.5;
}
buttonGroupAlpha = buttonPulseMinAlpha + s * (buttonPulseMaxAlpha - buttonPulseMinAlpha);
} else {
buttonGroupAlpha = 1.0;
}
// Update flash decay
if (buttonFlash > 0.0) {
buttonFlash -= frameMs * buttonFlashDecay;
if (buttonFlash < 0.0) buttonFlash = 0.0;
}
}
void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
@ -439,8 +558,8 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
// 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));
// Compute eased transition and delta to shift highscores when either options, level, or exit HUD is shown.
float combinedTransition = static_cast<float>(std::max(std::max(optionsTransition, levelTransition), exitTransition));
float eased = combinedTransition * combinedTransition * (3.0f - 2.0f * combinedTransition); // cubic smoothstep
float panelDelta = eased * moveAmount;
@ -617,15 +736,59 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
}
}
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
GameRenderer::renderExitPopup(
renderer,
ctx.pixelFont,
winW,
winH,
logicalScale,
ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1
);
// Inline exit HUD (no opaque background) - slides into the highscores area
if (exitTransition > 0.0) {
float easedE = static_cast<float>(exitTransition);
easedE = easedE * easedE * (3.0f - 2.0f * easedE);
const float panelW = 520.0f;
const float panelH = 360.0f;
float panelBaseX = (LOGICAL_W - panelW) * 0.5f + contentOffsetX;
float panelBaseY = (LOGICAL_H - panelH) * 0.5f + contentOffsetY - (LOGICAL_H * 0.10f);
float slideAmount = LOGICAL_H * 0.42f;
float panelY = panelBaseY + (1.0f - easedE) * slideAmount;
FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
if (titleFont) titleFont->draw(renderer, panelBaseX + 12.0f, panelY + 6.0f, "EXIT GAME?", 1.6f, SDL_Color{255,200,80,220});
SDL_FRect area{ panelBaseX + 12.0f, panelY + 56.0f, panelW - 24.0f, panelH - 120.0f };
// Sample long message (scrollable)
// Paragraph-style lines for a nicer exit confirmation message
std::vector<std::string> lines = {
"Quit now to return to your desktop. Your current session will end.",
"Press YES to quit immediately, or NO to return to the menu and continue playing.",
"Adjust audio, controls and other settings anytime from the Options menu.",
"Thanks for playing SPACETRIS — we hope to see you again!"
};
// Draw scrollable text (no background box; increased line spacing)
FontAtlas* f = ctx.font ? ctx.font : ctx.pixelFont;
float y = area.y - (float)exitScroll;
const float lineSpacing = 34.0f; // increased spacing for readability
if (f) {
for (size_t i = 0; i < lines.size(); ++i) {
f->draw(renderer, area.x + 6.0f, y + i * lineSpacing, lines[i], 1.0f, SDL_Color{200,220,240,220});
}
}
// Draw buttons at bottom of panel
float btnW2 = 160.0f, btnH2 = 48.0f;
float bx = panelBaseX + (panelW - (btnW2 * 2.0f + 12.0f)) * 0.5f;
float by = panelY + panelH - 56.0f;
// YES button
SDL_Color yesBg{220,80,60, 200}; SDL_Color yesBorder{160,40,40,200};
SDL_Color noBg{60,140,200,200}; SDL_Color noBorder{30,90,160,200};
// Apply pulse alpha to buttons
double aMul = std::clamp(buttonGroupAlpha + buttonFlash * buttonFlashAmount, 0.0, 1.0);
yesBg.a = static_cast<Uint8>(std::round(aMul * static_cast<double>(yesBg.a)));
yesBorder.a = static_cast<Uint8>(std::round(aMul * static_cast<double>(yesBorder.a)));
noBg.a = static_cast<Uint8>(std::round(aMul * static_cast<double>(noBg.a)));
noBorder.a = static_cast<Uint8>(std::round(aMul * static_cast<double>(noBorder.a)));
UIRenderer::drawButton(renderer, ctx.pixelFont, bx + btnW2*0.5f, by, btnW2, btnH2, "YES", false, exitSelectedButton == 0, yesBg, yesBorder, true, nullptr);
UIRenderer::drawButton(renderer, ctx.pixelFont, bx + btnW2*1.5f + 12.0f, by, btnW2, btnH2, "NO", false, exitSelectedButton == 1, noBg, noBorder, true, nullptr);
// Ensure ctx mirrors selection for any other code
if (ctx.exitPopupSelectedButton) *ctx.exitPopupSelectedButton = exitSelectedButton;
}
// Popups (settings only - level popup is now a separate state)

View File

@ -53,4 +53,25 @@ private:
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};
// Button group pulsing/fade parameters (applies to all four main buttons)
double buttonGroupAlpha = 1.0; // current computed alpha (0..1)
double buttonPulseTime = 0.0; // accumulator in ms
bool buttonPulseEnabled = true; // enable/disable pulsing
double buttonPulseSpeed = 1.0; // multiplier for pulse frequency
double buttonPulseMinAlpha = 0.60; // minimum alpha during pulse
double buttonPulseMaxAlpha = 1.00; // maximum alpha during pulse
// Pulse easing mode: 0=sin,1=triangle,2=exponential
int buttonPulseEasing = 1;
// Short bright flash when navigating buttons
double buttonFlash = 0.0; // transient flash amount (0..1)
double buttonFlashAmount = 0.45; // how much flash adds to group alpha
double buttonFlashDecay = 0.0025; // linear decay per ms
// Exit confirmation HUD state (inline HUD like Options/Level)
bool exitPanelVisible = false;
bool exitPanelAnimating = false;
double exitTransition = 0.0; // 0..1
double exitTransitionDurationMs = 360.0;
int exitDirection = 1; // 1 show, -1 hide
int exitSelectedButton = 0; // 0 = YES (quit), 1 = NO (cancel)
double exitScroll = 0.0; // vertical scroll offset for content
};