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:
2025-12-08 20:43:51 +01:00
parent 815913b15b
commit 57eac01bcb
8 changed files with 293 additions and 48 deletions

View File

@ -11,6 +11,8 @@ Sound=1
[Gameplay]
SmoothScroll=1
UpRotateClockwise=0
[Player]
Name=PLAYER

View File

@ -69,6 +69,8 @@ bool Settings::load() {
} else if (currentSection == "Gameplay") {
if (key == "SmoothScroll") {
m_smoothScrollEnabled = (value == "1" || value == "true" || value == "True");
} else if (key == "UpRotateClockwise") {
m_upRotateClockwise = (value == "1" || value == "true" || value == "True");
}
} else if (currentSection == "Player") {
if (key == "Name") {
@ -106,6 +108,7 @@ bool Settings::save() {
file << "[Gameplay]\n";
file << "SmoothScroll=" << (m_smoothScrollEnabled ? "1" : "0") << "\n\n";
file << "UpRotateClockwise=" << (m_upRotateClockwise ? "1" : "0") << "\n\n";
file << "[Player]\n";
file << "Name=" << m_playerName << "\n\n";

View File

@ -31,6 +31,10 @@ public:
bool isSmoothScrollEnabled() const { return m_smoothScrollEnabled; }
void setSmoothScrollEnabled(bool value) { m_smoothScrollEnabled = value; }
// Rotation behavior: should pressing UP rotate clockwise? (true = clockwise)
bool isUpRotateClockwise() const { return m_upRotateClockwise; }
void setUpRotateClockwise(bool value) { m_upRotateClockwise = value; }
const std::string& getPlayerName() const { return m_playerName; }
void setPlayerName(const std::string& name) { m_playerName = name; }
@ -50,4 +54,6 @@ private:
bool m_debugEnabled = false;
bool m_smoothScrollEnabled = true;
std::string m_playerName = "Player";
// Default: UP rotates clockwise
bool m_upRotateClockwise = true;
};

View File

@ -25,20 +25,13 @@ float fitScale(FontAtlas& font, const char* text, float initialScale, float maxW
}
return scale;
}
}
} // anonymous namespace
namespace HelpOverlay {
void Render(
SDL_Renderer* renderer,
FontAtlas& font,
float logicalWidth,
float logicalHeight,
float offsetX,
float offsetY) {
if (!renderer) {
return;
}
void Render(SDL_Renderer* renderer, FontAtlas& font, float logicalWidth, float logicalHeight, float offsetX, float offsetY) {
if (!renderer) return;
const std::array<ShortcutEntry, 5> generalShortcuts{{
{"H", "Toggle this help overlay"},
@ -58,7 +51,7 @@ void Render(
{"DOWN", "Soft drop (faster fall)"},
{"SPACE", "Hard drop / instant lock"},
{"UP", "Rotate clockwise"},
{"X", "Rotate counter-clockwise"},
{"X", "Toggle rotation direction used by UP"},
{"P", "Pause or resume"},
{"ESC", "Open exit confirmation"}
}};
@ -72,7 +65,8 @@ void Render(
drawRect(renderer, boxX - 2.0f, boxY - 2.0f, boxW + 4.0f, boxH + 4.0f, {10, 12, 20, 255});
drawRect(renderer, boxX, boxY, boxW, boxH, {18, 22, 35, 240});
const float titleScale = 1.7f;
// Slightly smaller overall title to fit tighter layouts
const float titleScale = 1.45f;
font.draw(renderer, boxX + 28.0f, boxY + 24.0f, "HELP & SHORTCUTS", titleScale, {255, 220, 0, 255});
const float contentPadding = 32.0f;
@ -83,24 +77,43 @@ void Render(
const float footerHeight = 46.0f;
const float footerPadding = 18.0f;
const float sectionTitleScale = 1.1f;
const float comboScale = 0.92f;
const float descBaseScale = 0.8f;
const float comboSpacing = 22.0f;
const float sectionSpacing = 14.0f;
// Slightly reduced scales for a more compact popup
const float sectionTitleScale = 1.0f;
const float comboScale = 0.82f;
const float descBaseScale = 0.72f;
// Increase vertical spacing between combo label and description for readability
const float comboSpacing = 28.0f;
// Increase spacing between sections and after titles
const float sectionSpacing = 22.0f;
// Helper to draw text with extra letter-spacing (tracking) for section titles
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]);
font.draw(renderer, x, sy, ch.c_str(), scale, color);
int cw = 0, chh = 0;
font.measure(ch.c_str(), scale, cw, chh);
x += static_cast<float>(cw) + extraPx;
}
};
auto drawSection = [&](float startX, float& cursorY, const char* title, const auto& entries) {
font.draw(renderer, startX, cursorY, title, sectionTitleScale, {180, 200, 255, 255});
cursorY += 26.0f;
drawSpaced(startX, cursorY, title, sectionTitleScale, {180, 200, 255, 255}, 4.0f);
// extra gap after section title
cursorY += 34.0f;
for (const auto& entry : entries) {
font.draw(renderer, startX, cursorY, entry.combo, comboScale, {255, 255, 255, 255});
// larger spacing between combo label and description
cursorY += comboSpacing;
float descScale = fitScale(font, entry.description, descBaseScale, columnWidth - 10.0f);
font.draw(renderer, startX, cursorY, entry.description, descScale, {200, 210, 230, 255});
int descW = 0, descH = 0;
font.measure(entry.description, descScale, descW, descH);
cursorY += static_cast<float>(descH) + 10.0f;
// a bit more space after description row
cursorY += static_cast<float>(descH) + 14.0f;
}
cursorY += sectionSpacing;
};
@ -121,7 +134,7 @@ void Render(
SDL_SetRenderDrawColor(renderer, 90, 110, 170, 255);
SDL_RenderRect(renderer, &footerRect);
const char* closeLabel = "PRESS H TO CLOSE";
const char* closeLabel = "PRESS H OR ESC TO CLOSE";
float closeScale = fitScale(font, closeLabel, 1.0f, footerRect.w - footerPadding * 2.0f);
int closeW = 0, closeH = 0;
font.measure(closeLabel, closeScale, closeW, closeH);

View File

@ -1039,7 +1039,8 @@ int main(int, char **)
SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled());
Settings::instance().setSoundEnabled(SoundEffectManager::instance().isEnabled());
}
if (e.key.scancode == SDL_SCANCODE_H && state != AppState::Loading)
// Disable H-help shortcut on the main menu; keep it elsewhere
if (e.key.scancode == SDL_SCANCODE_H && state != AppState::Loading && state != AppState::Menu)
{
showHelpOverlay = !showHelpOverlay;
if (state == AppState::Playing) {
@ -1058,6 +1059,25 @@ int main(int, char **)
helpOverlayPausedGame = false;
}
}
// If help overlay is visible and the user presses ESC, close help and return to Menu
if (e.key.scancode == SDL_SCANCODE_ESCAPE && showHelpOverlay) {
showHelpOverlay = false;
helpOverlayPausedGame = false;
// Unpause game if we paused it for the overlay
if (state == AppState::Playing) {
if (game.isPaused() && !helpOverlayPausedGame) {
// If paused for other reasons, avoid overriding; otherwise ensure unpaused
// (The flag helps detect pause because of help overlay.)
}
}
if (state != AppState::Menu && ctx.requestFadeTransition) {
// Request a transition back to the Menu state
ctx.requestFadeTransition(AppState::Menu);
} else if (state != AppState::Menu && ctx.stateManager) {
state = AppState::Menu;
ctx.stateManager->setState(state);
}
}
if (e.key.key == SDLK_F11 || (e.key.key == SDLK_RETURN && (e.key.mod & SDL_KMOD_ALT)))
{
isFullscreen = !isFullscreen;
@ -1160,9 +1180,9 @@ int main(int, char **)
const float btnYOffset = 40.0f; // must match MenuState offset
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
std::array<SDL_FRect, 4> buttonRects{};
for (int i = 0; i < 4; ++i) {
float center = btnCX + (static_cast<float>(i) - 1.5f) * spacing;
std::array<SDL_FRect, 5> buttonRects{};
for (int i = 0; i < 5; ++i) {
float center = btnCX + (static_cast<float>(i) - 2.0f) * spacing;
buttonRects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH};
}
@ -1177,6 +1197,9 @@ int main(int, char **)
} else if (pointInRect(buttonRects[2])) {
requestStateFade(AppState::Options);
} else if (pointInRect(buttonRects[3])) {
// HELP - show inline help HUD in the MenuState
if (menuState) menuState->showHelpPanel(true);
} else if (pointInRect(buttonRects[4])) {
showExitConfirmPopup = true;
exitPopupSelectedButton = 1;
}

View File

@ -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);

View File

@ -15,15 +15,18 @@ public:
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);
// Show or hide the inline HELP panel (menu-style)
void showHelpPanel(bool show);
private:
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = EXIT
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = HELP, 4 = EXIT
// Button icons (optional - will use text if nullptr)
SDL_Texture* playIcon = nullptr;
SDL_Texture* levelIcon = nullptr;
SDL_Texture* optionsIcon = nullptr;
SDL_Texture* exitIcon = nullptr;
SDL_Texture* helpIcon = nullptr;
// Options panel animation state
bool optionsVisible = false;
@ -74,4 +77,11 @@ private:
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
// Help submenu (inline HUD like Options/Exit)
bool helpPanelVisible = false;
bool helpPanelAnimating = false;
double helpTransition = 0.0; // 0..1
double helpTransitionDurationMs = 360.0;
int helpDirection = 1; // 1 show, -1 hide
double helpScroll = 0.0; // vertical scroll offset for content
};

View File

@ -6,6 +6,7 @@
#include "../audio/Audio.h"
#include "../audio/SoundEffect.h"
#include "../graphics/renderers/GameRenderer.h"
#include "../core/Settings.h"
#include "../core/Config.h"
#include <SDL3/SDL.h>
@ -119,11 +120,18 @@ void PlayingState::handleEvent(const SDL_Event& e) {
if (!ctx.game->isPaused()) {
// Rotation (still event-based for precise timing)
if (e.key.scancode == SDL_SCANCODE_UP) {
ctx.game->rotate(1); // Clockwise rotation
// Use user setting to determine whether UP rotates clockwise
bool upIsCW = Settings::instance().isUpRotateClockwise();
ctx.game->rotate(upIsCW ? 1 : -1);
return;
}
if (e.key.scancode == SDL_SCANCODE_X) {
ctx.game->rotate(-1); // Counter-clockwise rotation
// Toggle the mapping so UP will rotate in the opposite direction
bool current = Settings::instance().isUpRotateClockwise();
Settings::instance().setUpRotateClockwise(!current);
Settings::instance().save();
// Play a subtle feedback sound if available
SoundEffectManager::instance().playSound("menu_toggle", 0.6f);
return;
}