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]
|
||||
SmoothScroll=1
|
||||
|
||||
UpRotateClockwise=0
|
||||
|
||||
[Player]
|
||||
Name=PLAYER
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
31
src/main.cpp
31
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<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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user