feat(renderer): polish gameplay visuals — transport, starfield, sparkles, smooth piece motion
Add transport/transfer effect for NEXT → grid with cross-fade and preview swap Integrate in-grid Starfield3D with magnet targeting tied to active piece Spawn ambient sparkles and impact sparks (hard-drop crackle + burst on expiry) Smooth horizontal/fall interpolation for active piece (configurable smooth scroll) Refactor next panel / preview rendering and connector drawing Tweak stats/score panel layout, progress bars and typography for compact view Preserve safe alpha handling and restore renderer blend/scale state after overlays
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
#include "MenuState.h"
|
||||
#include "persistence/Scores.h"
|
||||
#include "graphics/Font.h"
|
||||
#include "../graphics/ui/HelpOverlay.h"
|
||||
#include "../core/GlobalState.h"
|
||||
#include "../core/Settings.h"
|
||||
#include "../core/state/StateManager.h"
|
||||
@ -106,6 +107,21 @@ static void renderBackdropBlur(SDL_Renderer* renderer, const SDL_Rect& logicalVP
|
||||
|
||||
MenuState::MenuState(StateContext& ctx) : State(ctx) {}
|
||||
|
||||
void MenuState::showHelpPanel(bool show) {
|
||||
if (show) {
|
||||
if (!helpPanelVisible && !helpPanelAnimating) {
|
||||
helpPanelAnimating = true;
|
||||
helpDirection = 1;
|
||||
helpScroll = 0.0;
|
||||
}
|
||||
} else {
|
||||
if (helpPanelVisible && !helpPanelAnimating) {
|
||||
helpPanelAnimating = true;
|
||||
helpDirection = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MenuState::onEnter() {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called");
|
||||
if (ctx.showExitConfirmPopup) {
|
||||
@ -139,14 +155,15 @@ void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale,
|
||||
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
||||
|
||||
struct MenuButtonDef { SDL_Color bg; SDL_Color border; std::string label; };
|
||||
std::array<MenuButtonDef,4> buttons = {
|
||||
std::array<MenuButtonDef,5> 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,200,60,255}, SDL_Color{150,150,40,255}, "HELP" },
|
||||
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
|
||||
};
|
||||
|
||||
std::array<SDL_Texture*,4> icons = { playIcon, levelIcon, optionsIcon, exitIcon };
|
||||
std::array<SDL_Texture*,5> icons = { playIcon, levelIcon, optionsIcon, helpIcon, exitIcon };
|
||||
|
||||
float spacing = isSmall ? btnW * 1.2f : btnW * 1.15f;
|
||||
|
||||
@ -185,8 +202,8 @@ void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale,
|
||||
groupCenterY = panelTop + panelH * 0.5f;
|
||||
}
|
||||
|
||||
// Draw all four buttons on top
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
// Draw all five buttons on top
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
float cxCenter = 0.0f;
|
||||
// Use the group's center Y so text/icons sit visually centered in the panel
|
||||
float cyCenter = groupCenterY;
|
||||
@ -194,12 +211,12 @@ void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale,
|
||||
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 offset = (static_cast<float>(i) - 2.0f) * spacing;
|
||||
// small per-button offsets to better match original art placement
|
||||
float extra = 0.0f;
|
||||
if (i == 0) extra = 15.0f;
|
||||
if (i == 2) extra = -24.0f;
|
||||
if (i == 3) extra = -44.0f;
|
||||
if (i == 2) extra = -18.0f;
|
||||
if (i == 4) extra = -24.0f;
|
||||
cxCenter = btnX + offset + extra;
|
||||
}
|
||||
// Apply group alpha and transient flash to button colors
|
||||
@ -227,6 +244,7 @@ void MenuState::onExit() {
|
||||
if (levelIcon) { SDL_DestroyTexture(levelIcon); levelIcon = nullptr; }
|
||||
if (optionsIcon) { SDL_DestroyTexture(optionsIcon); optionsIcon = nullptr; }
|
||||
if (exitIcon) { SDL_DestroyTexture(exitIcon); exitIcon = nullptr; }
|
||||
if (helpIcon) { SDL_DestroyTexture(helpIcon); helpIcon = nullptr; }
|
||||
}
|
||||
|
||||
void MenuState::handleEvent(const SDL_Event& e) {
|
||||
@ -373,6 +391,29 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
||||
}
|
||||
}
|
||||
|
||||
// If the inline help HUD is visible and not animating, capture navigation
|
||||
if (helpPanelVisible && !helpPanelAnimating) {
|
||||
switch (e.key.scancode) {
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
case SDL_SCANCODE_RETURN:
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
case SDL_SCANCODE_SPACE:
|
||||
// Close help panel
|
||||
helpPanelAnimating = true; helpDirection = -1;
|
||||
return;
|
||||
case SDL_SCANCODE_PAGEDOWN:
|
||||
case SDL_SCANCODE_DOWN: {
|
||||
helpScroll += 40.0; return;
|
||||
}
|
||||
case SDL_SCANCODE_PAGEUP:
|
||||
case SDL_SCANCODE_UP: {
|
||||
helpScroll -= 40.0; if (helpScroll < 0.0) helpScroll = 0.0; return;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If inline level HUD visible and not animating, capture navigation
|
||||
if (levelPanelVisible && !levelPanelAnimating) {
|
||||
// Start navigation from tentative hover if present, otherwise from committed selection
|
||||
@ -408,8 +449,8 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
||||
switch (e.key.scancode) {
|
||||
case SDL_SCANCODE_LEFT:
|
||||
case SDL_SCANCODE_UP:
|
||||
{
|
||||
const int total = 4;
|
||||
{
|
||||
const int total = 5;
|
||||
selectedButton = (selectedButton + total - 1) % total;
|
||||
// brief bright flash on navigation
|
||||
buttonFlash = 1.0;
|
||||
@ -417,8 +458,8 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
||||
}
|
||||
case SDL_SCANCODE_RIGHT:
|
||||
case SDL_SCANCODE_DOWN:
|
||||
{
|
||||
const int total = 4;
|
||||
{
|
||||
const int total = 5;
|
||||
selectedButton = (selectedButton + 1) % total;
|
||||
// brief bright flash on navigation
|
||||
buttonFlash = 1.0;
|
||||
@ -457,6 +498,17 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
// Toggle the inline HELP HUD (show/hide)
|
||||
if (!helpPanelVisible && !helpPanelAnimating) {
|
||||
helpPanelAnimating = true;
|
||||
helpDirection = 1; // show
|
||||
helpScroll = 0.0;
|
||||
} else if (helpPanelVisible && !helpPanelAnimating) {
|
||||
helpPanelAnimating = true;
|
||||
helpDirection = -1; // hide
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
// Show the inline exit HUD
|
||||
if (!exitPanelVisible && !exitPanelAnimating) {
|
||||
exitPanelAnimating = true;
|
||||
@ -538,6 +590,21 @@ void MenuState::update(double frameMs) {
|
||||
}
|
||||
}
|
||||
|
||||
// Advance help panel animation if active
|
||||
if (helpPanelAnimating) {
|
||||
double delta = (frameMs / helpTransitionDurationMs) * static_cast<double>(helpDirection);
|
||||
helpTransition += delta;
|
||||
if (helpTransition >= 1.0) {
|
||||
helpTransition = 1.0;
|
||||
helpPanelVisible = true;
|
||||
helpPanelAnimating = false;
|
||||
} else if (helpTransition <= 0.0) {
|
||||
helpTransition = 0.0;
|
||||
helpPanelVisible = false;
|
||||
helpPanelAnimating = 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
|
||||
@ -660,7 +727,7 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
const float moveAmount = 420.0f; // increased so lower score rows slide further up
|
||||
|
||||
// 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 combinedTransition = static_cast<float>(std::max(std::max(std::max(optionsTransition, levelTransition), exitTransition), helpTransition));
|
||||
float eased = combinedTransition * combinedTransition * (3.0f - 2.0f * combinedTransition); // cubic smoothstep
|
||||
float panelDelta = eased * moveAmount;
|
||||
|
||||
@ -829,18 +896,20 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
std::string label;
|
||||
};
|
||||
|
||||
std::array<MenuButtonDef, 4> buttons = {
|
||||
std::array<MenuButtonDef,5> 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,200,60,255}, SDL_Color{150,150,40,255}, "HELP" },
|
||||
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 = {
|
||||
std::array<SDL_Texture*, 5> icons = {
|
||||
playIcon,
|
||||
levelIcon,
|
||||
optionsIcon,
|
||||
helpIcon,
|
||||
exitIcon
|
||||
};
|
||||
|
||||
@ -852,7 +921,7 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
// Draw semi-transparent background panel behind the full button group
|
||||
{
|
||||
float groupCenterX = btnX;
|
||||
float halfSpan = 1.5f * spacing;
|
||||
float halfSpan = 2.0f * spacing;
|
||||
float panelLeft = groupCenterX - halfSpan - btnW * 0.5f - 14.0f;
|
||||
float panelRight = groupCenterX + halfSpan + btnW * 0.5f + 14.0f;
|
||||
float panelTop = btnY - btnH * 0.5f - 12.0f;
|
||||
@ -936,12 +1005,29 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
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;
|
||||
float offset = (static_cast<float>(i) - 2.0f) * 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 4 - EXIT
|
||||
{
|
||||
const int i = 4;
|
||||
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) - 2.0f) * 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1084,6 +1170,100 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
drawField(4, rowY, "", "RETURN TO MENU");
|
||||
}
|
||||
|
||||
// Draw inline help HUD (no boxed background) — match Options/Exit style
|
||||
if (helpTransition > 0.0) {
|
||||
float easedH = static_cast<float>(helpTransition);
|
||||
easedH = easedH * easedH * (3.0f - 2.0f * easedH);
|
||||
const float PW = std::min(520.0f, LOGICAL_W * 0.65f);
|
||||
const float PH = std::min(420.0f, LOGICAL_H * 0.72f);
|
||||
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 - easedH) * slideAmount;
|
||||
|
||||
FontAtlas* f = ctx.pixelFont ? ctx.pixelFont : ctx.font;
|
||||
if (f) {
|
||||
// Header (smaller)
|
||||
f->draw(renderer, panelBaseX + 12.0f, panelY + 6.0f, "HELP & SHORTCUTS", 1.25f, SDL_Color{255,220,0,255});
|
||||
|
||||
// Content layout (two columns)
|
||||
const float contentPadding = 16.0f;
|
||||
const float columnGap = 18.0f;
|
||||
const float columnWidth = (PW - contentPadding * 2.0f - columnGap) * 0.5f;
|
||||
const float leftX = panelBaseX + contentPadding;
|
||||
const float rightX = leftX + columnWidth + columnGap;
|
||||
|
||||
// Shortcut entries (copied from HelpOverlay)
|
||||
struct ShortcutEntry { const char* combo; const char* description; };
|
||||
const ShortcutEntry generalShortcuts[] = {
|
||||
{"H", "Toggle this help overlay"},
|
||||
{"ESC", "Back / cancel current popup"},
|
||||
{"F11 or ALT+ENTER", "Toggle fullscreen"},
|
||||
{"M", "Mute or unmute music"},
|
||||
{"S", "Toggle sound effects"}
|
||||
};
|
||||
const ShortcutEntry menuShortcuts[] = {
|
||||
{"ARROW KEYS", "Navigate menu buttons"},
|
||||
{"ENTER / SPACE", "Activate highlighted action"}
|
||||
};
|
||||
const ShortcutEntry gameplayShortcuts[] = {
|
||||
{"LEFT / RIGHT", "Move active piece"},
|
||||
{"DOWN", "Soft drop (faster fall)"},
|
||||
{"SPACE", "Hard drop / instant lock"},
|
||||
{"UP", "Rotate clockwise"},
|
||||
{"X", "Toggle rotation direction used by UP"},
|
||||
{"P", "Pause or resume"},
|
||||
{"ESC", "Open exit confirmation"}
|
||||
};
|
||||
|
||||
// Helper to draw text with extra letter-spacing (tracking)
|
||||
auto drawSpaced = [&](float sx, float sy, const char* text, float scale, SDL_Color color, float extraPx) {
|
||||
std::string stext(text);
|
||||
float x = sx;
|
||||
for (size_t i = 0; i < stext.size(); ++i) {
|
||||
std::string ch(1, stext[i]);
|
||||
f->draw(renderer, x, sy, ch.c_str(), scale, color);
|
||||
int cw = 0, chh = 0;
|
||||
f->measure(ch.c_str(), scale, cw, chh);
|
||||
x += static_cast<float>(cw) + extraPx;
|
||||
}
|
||||
};
|
||||
|
||||
auto drawSection = [&](float sx, float& cursorY, const char* title, const ShortcutEntry* entries, int count) {
|
||||
// Section title (smaller) with added letter spacing (reduced scale)
|
||||
drawSpaced(sx, cursorY, title, 0.85f, SDL_Color{180,200,255,255}, 4.0f);
|
||||
// Add extra gap after the headline so it separates clearly from the first row
|
||||
cursorY += 28.0f;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const auto &entry = entries[i];
|
||||
// Combo/key label
|
||||
f->draw(renderer, sx, cursorY, entry.combo, 0.70f, SDL_Color{255,255,255,255});
|
||||
// Slightly more space between the combo/key and the description
|
||||
cursorY += 26.0f;
|
||||
|
||||
// Description (smaller) with increased spacing
|
||||
f->draw(renderer, sx + 6.0f, cursorY, entry.description, 0.62f, SDL_Color{200,210,230,255});
|
||||
int w=0,h=0; f->measure(entry.description, 0.62f, w, h);
|
||||
cursorY += static_cast<float>(h) + 16.0f;
|
||||
}
|
||||
// Add a larger gap between sections
|
||||
cursorY += 22.0f;
|
||||
};
|
||||
|
||||
float leftCursor = panelY + 48.0f - static_cast<float>(helpScroll);
|
||||
float rightCursor = panelY + 48.0f - static_cast<float>(helpScroll);
|
||||
drawSection(leftX, leftCursor, "GENERAL", generalShortcuts, (int)(sizeof(generalShortcuts)/sizeof(generalShortcuts[0])));
|
||||
drawSection(leftX, leftCursor, "MENUS", menuShortcuts, (int)(sizeof(menuShortcuts)/sizeof(menuShortcuts[0])));
|
||||
drawSection(rightX, rightCursor, "GAMEPLAY", gameplayShortcuts, (int)(sizeof(gameplayShortcuts)/sizeof(gameplayShortcuts[0])));
|
||||
|
||||
// Ensure helpScroll bounds (simple clamp)
|
||||
float contentHeight = std::max(leftCursor, rightCursor) - (panelY + 48.0f);
|
||||
float maxScroll = std::max(0.0f, contentHeight - (PH - 120.0f));
|
||||
if (helpScroll < 0.0) helpScroll = 0.0;
|
||||
if (helpScroll > maxScroll) helpScroll = maxScroll;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw inline level selector HUD (no background) if active
|
||||
if (levelTransition > 0.0) {
|
||||
float easedL = static_cast<float>(levelTransition);
|
||||
|
||||
Reference in New Issue
Block a user