From 57eac01bcb0be72a19a1471be367b34aae75920e Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Mon, 8 Dec 2025 20:43:51 +0100 Subject: [PATCH] =?UTF-8?q?feat(renderer):=20polish=20gameplay=20visuals?= =?UTF-8?q?=20=E2=80=94=20transport,=20starfield,=20sparkles,=20smooth=20p?= =?UTF-8?q?iece=20motion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- settings.ini | 2 + src/core/Settings.cpp | 3 + src/core/Settings.h | 6 + src/graphics/ui/HelpOverlay.cpp | 57 +++++---- src/main.cpp | 31 ++++- src/states/MenuState.cpp | 218 +++++++++++++++++++++++++++++--- src/states/MenuState.h | 12 +- src/states/PlayingState.cpp | 12 +- 8 files changed, 293 insertions(+), 48 deletions(-) diff --git a/settings.ini b/settings.ini index 384ce2a..cf57a4c 100644 --- a/settings.ini +++ b/settings.ini @@ -11,6 +11,8 @@ Sound=1 [Gameplay] SmoothScroll=1 +UpRotateClockwise=0 + [Player] Name=PLAYER diff --git a/src/core/Settings.cpp b/src/core/Settings.cpp index 2a24320..2f5e200 100644 --- a/src/core/Settings.cpp +++ b/src/core/Settings.cpp @@ -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"; diff --git a/src/core/Settings.h b/src/core/Settings.h index 048bd81..f29d75b 100644 --- a/src/core/Settings.h +++ b/src/core/Settings.h @@ -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; }; diff --git a/src/graphics/ui/HelpOverlay.cpp b/src/graphics/ui/HelpOverlay.cpp index b1469d6..3afdf2b 100644 --- a/src/graphics/ui/HelpOverlay.cpp +++ b/src/graphics/ui/HelpOverlay.cpp @@ -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 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(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(descH) + 10.0f; + // a bit more space after description row + cursorY += static_cast(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); diff --git a/src/main.cpp b/src/main.cpp index 2c7e876..dfa928d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 buttonRects{}; - for (int i = 0; i < 4; ++i) { - float center = btnCX + (static_cast(i) - 1.5f) * spacing; + std::array buttonRects{}; + for (int i = 0; i < 5; ++i) { + float center = btnCX + (static_cast(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; } diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 638bce1..2f731a7 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -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 buttons = { + std::array 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 icons = { playIcon, levelIcon, optionsIcon, exitIcon }; + std::array 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(i) - 1.5f) * spacing; - // Apply small per-button offsets that match the original placements + float offset = (static_cast(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(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(std::max(std::max(optionsTransition, levelTransition), exitTransition)); + float combinedTransition = static_cast(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 buttons = { + std::array 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 icons = { + std::array 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(i) - 1.5f) * spacing; - cxCenter = btnX + offset - 44.0f; + float offset = (static_cast(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(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(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(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(h) + 16.0f; + } + // Add a larger gap between sections + cursorY += 22.0f; + }; + + float leftCursor = panelY + 48.0f - static_cast(helpScroll); + float rightCursor = panelY + 48.0f - static_cast(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(levelTransition); diff --git a/src/states/MenuState.h b/src/states/MenuState.h index 0f5f5c1..32243d9 100644 --- a/src/states/MenuState.h +++ b/src/states/MenuState.h @@ -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 }; diff --git a/src/states/PlayingState.cpp b/src/states/PlayingState.cpp index 4e846b1..2ea5417 100644 --- a/src/states/PlayingState.cpp +++ b/src/states/PlayingState.cpp @@ -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 @@ -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; }