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:
@ -11,6 +11,8 @@ Sound=1
|
|||||||
[Gameplay]
|
[Gameplay]
|
||||||
SmoothScroll=1
|
SmoothScroll=1
|
||||||
|
|
||||||
|
UpRotateClockwise=0
|
||||||
|
|
||||||
[Player]
|
[Player]
|
||||||
Name=PLAYER
|
Name=PLAYER
|
||||||
|
|
||||||
|
|||||||
@ -69,6 +69,8 @@ bool Settings::load() {
|
|||||||
} else if (currentSection == "Gameplay") {
|
} else if (currentSection == "Gameplay") {
|
||||||
if (key == "SmoothScroll") {
|
if (key == "SmoothScroll") {
|
||||||
m_smoothScrollEnabled = (value == "1" || value == "true" || value == "True");
|
m_smoothScrollEnabled = (value == "1" || value == "true" || value == "True");
|
||||||
|
} else if (key == "UpRotateClockwise") {
|
||||||
|
m_upRotateClockwise = (value == "1" || value == "true" || value == "True");
|
||||||
}
|
}
|
||||||
} else if (currentSection == "Player") {
|
} else if (currentSection == "Player") {
|
||||||
if (key == "Name") {
|
if (key == "Name") {
|
||||||
@ -106,6 +108,7 @@ bool Settings::save() {
|
|||||||
|
|
||||||
file << "[Gameplay]\n";
|
file << "[Gameplay]\n";
|
||||||
file << "SmoothScroll=" << (m_smoothScrollEnabled ? "1" : "0") << "\n\n";
|
file << "SmoothScroll=" << (m_smoothScrollEnabled ? "1" : "0") << "\n\n";
|
||||||
|
file << "UpRotateClockwise=" << (m_upRotateClockwise ? "1" : "0") << "\n\n";
|
||||||
|
|
||||||
file << "[Player]\n";
|
file << "[Player]\n";
|
||||||
file << "Name=" << m_playerName << "\n\n";
|
file << "Name=" << m_playerName << "\n\n";
|
||||||
|
|||||||
@ -31,6 +31,10 @@ public:
|
|||||||
|
|
||||||
bool isSmoothScrollEnabled() const { return m_smoothScrollEnabled; }
|
bool isSmoothScrollEnabled() const { return m_smoothScrollEnabled; }
|
||||||
void setSmoothScrollEnabled(bool value) { m_smoothScrollEnabled = value; }
|
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; }
|
const std::string& getPlayerName() const { return m_playerName; }
|
||||||
void setPlayerName(const std::string& name) { m_playerName = name; }
|
void setPlayerName(const std::string& name) { m_playerName = name; }
|
||||||
@ -50,4 +54,6 @@ private:
|
|||||||
bool m_debugEnabled = false;
|
bool m_debugEnabled = false;
|
||||||
bool m_smoothScrollEnabled = true;
|
bool m_smoothScrollEnabled = true;
|
||||||
std::string m_playerName = "Player";
|
std::string m_playerName = "Player";
|
||||||
|
// Default: UP rotates clockwise
|
||||||
|
bool m_upRotateClockwise = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,20 +25,13 @@ float fitScale(FontAtlas& font, const char* text, float initialScale, float maxW
|
|||||||
}
|
}
|
||||||
return scale;
|
return scale;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
namespace HelpOverlay {
|
namespace HelpOverlay {
|
||||||
|
|
||||||
void Render(
|
void Render(SDL_Renderer* renderer, FontAtlas& font, float logicalWidth, float logicalHeight, float offsetX, float offsetY) {
|
||||||
SDL_Renderer* renderer,
|
if (!renderer) return;
|
||||||
FontAtlas& font,
|
|
||||||
float logicalWidth,
|
|
||||||
float logicalHeight,
|
|
||||||
float offsetX,
|
|
||||||
float offsetY) {
|
|
||||||
if (!renderer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::array<ShortcutEntry, 5> generalShortcuts{{
|
const std::array<ShortcutEntry, 5> generalShortcuts{{
|
||||||
{"H", "Toggle this help overlay"},
|
{"H", "Toggle this help overlay"},
|
||||||
@ -58,7 +51,7 @@ void Render(
|
|||||||
{"DOWN", "Soft drop (faster fall)"},
|
{"DOWN", "Soft drop (faster fall)"},
|
||||||
{"SPACE", "Hard drop / instant lock"},
|
{"SPACE", "Hard drop / instant lock"},
|
||||||
{"UP", "Rotate clockwise"},
|
{"UP", "Rotate clockwise"},
|
||||||
{"X", "Rotate counter-clockwise"},
|
{"X", "Toggle rotation direction used by UP"},
|
||||||
{"P", "Pause or resume"},
|
{"P", "Pause or resume"},
|
||||||
{"ESC", "Open exit confirmation"}
|
{"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 - 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});
|
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});
|
font.draw(renderer, boxX + 28.0f, boxY + 24.0f, "HELP & SHORTCUTS", titleScale, {255, 220, 0, 255});
|
||||||
|
|
||||||
const float contentPadding = 32.0f;
|
const float contentPadding = 32.0f;
|
||||||
@ -83,24 +77,43 @@ void Render(
|
|||||||
const float footerHeight = 46.0f;
|
const float footerHeight = 46.0f;
|
||||||
const float footerPadding = 18.0f;
|
const float footerPadding = 18.0f;
|
||||||
|
|
||||||
const float sectionTitleScale = 1.1f;
|
// Slightly reduced scales for a more compact popup
|
||||||
const float comboScale = 0.92f;
|
const float sectionTitleScale = 1.0f;
|
||||||
const float descBaseScale = 0.8f;
|
const float comboScale = 0.82f;
|
||||||
const float comboSpacing = 22.0f;
|
const float descBaseScale = 0.72f;
|
||||||
const float sectionSpacing = 14.0f;
|
// 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) {
|
auto drawSection = [&](float startX, float& cursorY, const char* title, const auto& entries) {
|
||||||
font.draw(renderer, startX, cursorY, title, sectionTitleScale, {180, 200, 255, 255});
|
drawSpaced(startX, cursorY, title, sectionTitleScale, {180, 200, 255, 255}, 4.0f);
|
||||||
cursorY += 26.0f;
|
// extra gap after section title
|
||||||
|
cursorY += 34.0f;
|
||||||
for (const auto& entry : entries) {
|
for (const auto& entry : entries) {
|
||||||
font.draw(renderer, startX, cursorY, entry.combo, comboScale, {255, 255, 255, 255});
|
font.draw(renderer, startX, cursorY, entry.combo, comboScale, {255, 255, 255, 255});
|
||||||
|
// larger spacing between combo label and description
|
||||||
cursorY += comboSpacing;
|
cursorY += comboSpacing;
|
||||||
|
|
||||||
float descScale = fitScale(font, entry.description, descBaseScale, columnWidth - 10.0f);
|
float descScale = fitScale(font, entry.description, descBaseScale, columnWidth - 10.0f);
|
||||||
font.draw(renderer, startX, cursorY, entry.description, descScale, {200, 210, 230, 255});
|
font.draw(renderer, startX, cursorY, entry.description, descScale, {200, 210, 230, 255});
|
||||||
int descW = 0, descH = 0;
|
int descW = 0, descH = 0;
|
||||||
font.measure(entry.description, descScale, descW, descH);
|
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;
|
cursorY += sectionSpacing;
|
||||||
};
|
};
|
||||||
@ -121,7 +134,7 @@ void Render(
|
|||||||
SDL_SetRenderDrawColor(renderer, 90, 110, 170, 255);
|
SDL_SetRenderDrawColor(renderer, 90, 110, 170, 255);
|
||||||
SDL_RenderRect(renderer, &footerRect);
|
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);
|
float closeScale = fitScale(font, closeLabel, 1.0f, footerRect.w - footerPadding * 2.0f);
|
||||||
int closeW = 0, closeH = 0;
|
int closeW = 0, closeH = 0;
|
||||||
font.measure(closeLabel, closeScale, closeW, closeH);
|
font.measure(closeLabel, closeScale, closeW, closeH);
|
||||||
|
|||||||
31
src/main.cpp
31
src/main.cpp
@ -1039,7 +1039,8 @@ int main(int, char **)
|
|||||||
SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled());
|
SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled());
|
||||||
Settings::instance().setSoundEnabled(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;
|
showHelpOverlay = !showHelpOverlay;
|
||||||
if (state == AppState::Playing) {
|
if (state == AppState::Playing) {
|
||||||
@ -1058,6 +1059,25 @@ int main(int, char **)
|
|||||||
helpOverlayPausedGame = false;
|
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)))
|
if (e.key.key == SDLK_F11 || (e.key.key == SDLK_RETURN && (e.key.mod & SDL_KMOD_ALT)))
|
||||||
{
|
{
|
||||||
isFullscreen = !isFullscreen;
|
isFullscreen = !isFullscreen;
|
||||||
@ -1160,9 +1180,9 @@ int main(int, char **)
|
|||||||
const float btnYOffset = 40.0f; // must match MenuState offset
|
const float btnYOffset = 40.0f; // must match MenuState offset
|
||||||
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
float btnCY = LOGICAL_H * 0.86f + contentOffsetY + btnYOffset;
|
||||||
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
|
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
|
||||||
std::array<SDL_FRect, 4> buttonRects{};
|
std::array<SDL_FRect, 5> buttonRects{};
|
||||||
for (int i = 0; i < 4; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
float center = btnCX + (static_cast<float>(i) - 1.5f) * spacing;
|
float center = btnCX + (static_cast<float>(i) - 2.0f) * spacing;
|
||||||
buttonRects[i] = SDL_FRect{center - btnW / 2.0f, btnCY - btnH / 2.0f, btnW, btnH};
|
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])) {
|
} else if (pointInRect(buttonRects[2])) {
|
||||||
requestStateFade(AppState::Options);
|
requestStateFade(AppState::Options);
|
||||||
} else if (pointInRect(buttonRects[3])) {
|
} 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;
|
showExitConfirmPopup = true;
|
||||||
exitPopupSelectedButton = 1;
|
exitPopupSelectedButton = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "MenuState.h"
|
#include "MenuState.h"
|
||||||
#include "persistence/Scores.h"
|
#include "persistence/Scores.h"
|
||||||
#include "graphics/Font.h"
|
#include "graphics/Font.h"
|
||||||
|
#include "../graphics/ui/HelpOverlay.h"
|
||||||
#include "../core/GlobalState.h"
|
#include "../core/GlobalState.h"
|
||||||
#include "../core/Settings.h"
|
#include "../core/Settings.h"
|
||||||
#include "../core/state/StateManager.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) {}
|
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() {
|
void MenuState::onEnter() {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called");
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called");
|
||||||
if (ctx.showExitConfirmPopup) {
|
if (ctx.showExitConfirmPopup) {
|
||||||
@ -139,14 +155,15 @@ void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale,
|
|||||||
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
||||||
|
|
||||||
struct MenuButtonDef { SDL_Color bg; SDL_Color border; std::string label; };
|
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{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{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{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" }
|
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;
|
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;
|
groupCenterY = panelTop + panelH * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw all four buttons on top
|
// Draw all five buttons on top
|
||||||
for (int i = 0; i < 4; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
float cxCenter = 0.0f;
|
float cxCenter = 0.0f;
|
||||||
// Use the group's center Y so text/icons sit visually centered in the panel
|
// Use the group's center Y so text/icons sit visually centered in the panel
|
||||||
float cyCenter = groupCenterY;
|
float cyCenter = groupCenterY;
|
||||||
@ -194,12 +211,12 @@ void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale,
|
|||||||
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
|
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
|
||||||
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
|
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
|
||||||
} else {
|
} else {
|
||||||
float offset = (static_cast<float>(i) - 1.5f) * spacing;
|
float offset = (static_cast<float>(i) - 2.0f) * spacing;
|
||||||
// Apply small per-button offsets that match the original placements
|
// small per-button offsets to better match original art placement
|
||||||
float extra = 0.0f;
|
float extra = 0.0f;
|
||||||
if (i == 0) extra = 15.0f;
|
if (i == 0) extra = 15.0f;
|
||||||
if (i == 2) extra = -24.0f;
|
if (i == 2) extra = -18.0f;
|
||||||
if (i == 3) extra = -44.0f;
|
if (i == 4) extra = -24.0f;
|
||||||
cxCenter = btnX + offset + extra;
|
cxCenter = btnX + offset + extra;
|
||||||
}
|
}
|
||||||
// Apply group alpha and transient flash to button colors
|
// Apply group alpha and transient flash to button colors
|
||||||
@ -227,6 +244,7 @@ void MenuState::onExit() {
|
|||||||
if (levelIcon) { SDL_DestroyTexture(levelIcon); levelIcon = nullptr; }
|
if (levelIcon) { SDL_DestroyTexture(levelIcon); levelIcon = nullptr; }
|
||||||
if (optionsIcon) { SDL_DestroyTexture(optionsIcon); optionsIcon = nullptr; }
|
if (optionsIcon) { SDL_DestroyTexture(optionsIcon); optionsIcon = nullptr; }
|
||||||
if (exitIcon) { SDL_DestroyTexture(exitIcon); exitIcon = nullptr; }
|
if (exitIcon) { SDL_DestroyTexture(exitIcon); exitIcon = nullptr; }
|
||||||
|
if (helpIcon) { SDL_DestroyTexture(helpIcon); helpIcon = nullptr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuState::handleEvent(const SDL_Event& e) {
|
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 inline level HUD visible and not animating, capture navigation
|
||||||
if (levelPanelVisible && !levelPanelAnimating) {
|
if (levelPanelVisible && !levelPanelAnimating) {
|
||||||
// Start navigation from tentative hover if present, otherwise from committed selection
|
// 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) {
|
switch (e.key.scancode) {
|
||||||
case SDL_SCANCODE_LEFT:
|
case SDL_SCANCODE_LEFT:
|
||||||
case SDL_SCANCODE_UP:
|
case SDL_SCANCODE_UP:
|
||||||
{
|
{
|
||||||
const int total = 4;
|
const int total = 5;
|
||||||
selectedButton = (selectedButton + total - 1) % total;
|
selectedButton = (selectedButton + total - 1) % total;
|
||||||
// brief bright flash on navigation
|
// brief bright flash on navigation
|
||||||
buttonFlash = 1.0;
|
buttonFlash = 1.0;
|
||||||
@ -417,8 +458,8 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
|||||||
}
|
}
|
||||||
case SDL_SCANCODE_RIGHT:
|
case SDL_SCANCODE_RIGHT:
|
||||||
case SDL_SCANCODE_DOWN:
|
case SDL_SCANCODE_DOWN:
|
||||||
{
|
{
|
||||||
const int total = 4;
|
const int total = 5;
|
||||||
selectedButton = (selectedButton + 1) % total;
|
selectedButton = (selectedButton + 1) % total;
|
||||||
// brief bright flash on navigation
|
// brief bright flash on navigation
|
||||||
buttonFlash = 1.0;
|
buttonFlash = 1.0;
|
||||||
@ -457,6 +498,17 @@ void MenuState::handleEvent(const SDL_Event& e) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3:
|
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
|
// Show the inline exit HUD
|
||||||
if (!exitPanelVisible && !exitPanelAnimating) {
|
if (!exitPanelVisible && !exitPanelAnimating) {
|
||||||
exitPanelAnimating = true;
|
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
|
// Animate level selection highlight position toward the selected cell center
|
||||||
if (levelTransition > 0.0 && (lastLogicalScale > 0.0f)) {
|
if (levelTransition > 0.0 && (lastLogicalScale > 0.0f)) {
|
||||||
// Recompute same grid geometry used in render to find target center
|
// 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
|
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.
|
// 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 eased = combinedTransition * combinedTransition * (3.0f - 2.0f * combinedTransition); // cubic smoothstep
|
||||||
float panelDelta = eased * moveAmount;
|
float panelDelta = eased * moveAmount;
|
||||||
|
|
||||||
@ -829,18 +896,20 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
|||||||
std::string label;
|
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{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{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{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" }
|
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Icon array (nullptr if icon not loaded)
|
// Icon array (nullptr if icon not loaded)
|
||||||
std::array<SDL_Texture*, 4> icons = {
|
std::array<SDL_Texture*, 5> icons = {
|
||||||
playIcon,
|
playIcon,
|
||||||
levelIcon,
|
levelIcon,
|
||||||
optionsIcon,
|
optionsIcon,
|
||||||
|
helpIcon,
|
||||||
exitIcon
|
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
|
// Draw semi-transparent background panel behind the full button group
|
||||||
{
|
{
|
||||||
float groupCenterX = btnX;
|
float groupCenterX = btnX;
|
||||||
float halfSpan = 1.5f * spacing;
|
float halfSpan = 2.0f * spacing;
|
||||||
float panelLeft = groupCenterX - halfSpan - btnW * 0.5f - 14.0f;
|
float panelLeft = groupCenterX - halfSpan - btnW * 0.5f - 14.0f;
|
||||||
float panelRight = groupCenterX + halfSpan + btnW * 0.5f + 14.0f;
|
float panelRight = groupCenterX + halfSpan + btnW * 0.5f + 14.0f;
|
||||||
float panelTop = btnY - btnH * 0.5f - 12.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;
|
cxCenter = ctx.menuButtonCX[i] + contentOffsetX;
|
||||||
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
|
cyCenter = ctx.menuButtonCY[i] + contentOffsetY;
|
||||||
} else {
|
} else {
|
||||||
float offset = (static_cast<float>(i) - 1.5f) * spacing;
|
float offset = (static_cast<float>(i) - 2.0f) * spacing;
|
||||||
cxCenter = btnX + offset - 44.0f;
|
cxCenter = btnX + offset;
|
||||||
}
|
}
|
||||||
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
|
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
|
||||||
buttons[i].label, false, selectedButton == i,
|
buttons[i].label, false, selectedButton == i,
|
||||||
buttons[i].bg, buttons[i].border, true, icons[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");
|
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
|
// Draw inline level selector HUD (no background) if active
|
||||||
if (levelTransition > 0.0) {
|
if (levelTransition > 0.0) {
|
||||||
float easedL = static_cast<float>(levelTransition);
|
float easedL = static_cast<float>(levelTransition);
|
||||||
|
|||||||
@ -15,15 +15,18 @@ public:
|
|||||||
bool drawMainButtonNormally = true;
|
bool drawMainButtonNormally = true;
|
||||||
// Draw only the main PLAY button on top of other layers (expects logical coords).
|
// Draw only the main PLAY button on top of other layers (expects logical coords).
|
||||||
void renderMainButtonTop(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP);
|
void renderMainButtonTop(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP);
|
||||||
|
// Show or hide the inline HELP panel (menu-style)
|
||||||
|
void showHelpPanel(bool show);
|
||||||
|
|
||||||
private:
|
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)
|
// Button icons (optional - will use text if nullptr)
|
||||||
SDL_Texture* playIcon = nullptr;
|
SDL_Texture* playIcon = nullptr;
|
||||||
SDL_Texture* levelIcon = nullptr;
|
SDL_Texture* levelIcon = nullptr;
|
||||||
SDL_Texture* optionsIcon = nullptr;
|
SDL_Texture* optionsIcon = nullptr;
|
||||||
SDL_Texture* exitIcon = nullptr;
|
SDL_Texture* exitIcon = nullptr;
|
||||||
|
SDL_Texture* helpIcon = nullptr;
|
||||||
|
|
||||||
// Options panel animation state
|
// Options panel animation state
|
||||||
bool optionsVisible = false;
|
bool optionsVisible = false;
|
||||||
@ -74,4 +77,11 @@ private:
|
|||||||
int exitDirection = 1; // 1 show, -1 hide
|
int exitDirection = 1; // 1 show, -1 hide
|
||||||
int exitSelectedButton = 0; // 0 = YES (quit), 1 = NO (cancel)
|
int exitSelectedButton = 0; // 0 = YES (quit), 1 = NO (cancel)
|
||||||
double exitScroll = 0.0; // vertical scroll offset for content
|
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
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include "../audio/Audio.h"
|
#include "../audio/Audio.h"
|
||||||
#include "../audio/SoundEffect.h"
|
#include "../audio/SoundEffect.h"
|
||||||
#include "../graphics/renderers/GameRenderer.h"
|
#include "../graphics/renderers/GameRenderer.h"
|
||||||
|
#include "../core/Settings.h"
|
||||||
#include "../core/Config.h"
|
#include "../core/Config.h"
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
@ -119,11 +120,18 @@ void PlayingState::handleEvent(const SDL_Event& e) {
|
|||||||
if (!ctx.game->isPaused()) {
|
if (!ctx.game->isPaused()) {
|
||||||
// Rotation (still event-based for precise timing)
|
// Rotation (still event-based for precise timing)
|
||||||
if (e.key.scancode == SDL_SCANCODE_UP) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (e.key.scancode == SDL_SCANCODE_X) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user