From 588f870b264ac4f01ffa296c5fe9e2bfa9fef8af Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sun, 30 Nov 2025 12:29:09 +0100 Subject: [PATCH 1/3] smooth scroll left / right --- settings.ini | 2 +- src/gameplay/core/Game.cpp | 5 ++- src/gameplay/core/Game.h | 2 ++ src/graphics/renderers/GameRenderer.cpp | 43 +++++++++++++++++++++---- src/graphics/renderers/GameRenderer.h | 2 +- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/settings.ini b/settings.ini index 200523e..384ce2a 100644 --- a/settings.ini +++ b/settings.ini @@ -12,7 +12,7 @@ Sound=1 SmoothScroll=1 [Player] -Name=GREGOR +Name=PLAYER [Debug] Enabled=1 diff --git a/src/gameplay/core/Game.cpp b/src/gameplay/core/Game.cpp index 8342ce0..ce174db 100644 --- a/src/gameplay/core/Game.cpp +++ b/src/gameplay/core/Game.cpp @@ -64,7 +64,9 @@ void Game::reset(int startLevel_) { _pausedTime = 0; _lastPauseStart = 0; hold = Piece{}; hold.type = PIECE_COUNT; canHold=true; - refillBag(); spawn(); + refillBag(); + pieceSequence = 0; + spawn(); } double Game::elapsed() const { @@ -166,6 +168,7 @@ void Game::spawn() { PieceType nextType = bag.back(); int nextSpawnY = (nextType == I) ? -2 : -1; nextPiece = Piece{ nextType, 0, 3, nextSpawnY }; + ++pieceSequence; } bool Game::cellFilled(const Piece& p, int cx, int cy) { diff --git a/src/gameplay/core/Game.h b/src/gameplay/core/Game.h index 8fd26b1..63774bc 100644 --- a/src/gameplay/core/Game.h +++ b/src/gameplay/core/Game.h @@ -80,6 +80,7 @@ public: double hardDropShakeStrength() const; const std::vector& getHardDropCells() const { return hardDropCells; } uint32_t getHardDropFxId() const { return hardDropFxId; } + uint64_t getCurrentPieceSequence() const { return pieceSequence; } private: std::array board{}; // 0 empty else color index @@ -121,6 +122,7 @@ private: static constexpr double HARD_DROP_SHAKE_DURATION_MS = 320.0; std::vector hardDropCells; uint32_t hardDropFxId{0}; + uint64_t pieceSequence{0}; // Internal helpers ---------------------------------------------------- void refillBag(); diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index b280d37..52bdd9d 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,14 @@ struct ImpactSpark { float size = 0.0f; SDL_Color color{255, 255, 255, 255}; }; + +struct ActivePieceSmoothState { + uint64_t sequence = 0; + float visualX = 0.0f; + bool initialized = false; +}; + +ActivePieceSmoothState s_activePieceSmooth; } // Color constants (copied from main.cpp) @@ -62,13 +71,13 @@ void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksT SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect); } -void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost, float pixelOffsetY) { +void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost, float pixelOffsetX, float pixelOffsetY) { if (piece.type >= PIECE_COUNT) return; for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (Game::cellFilled(piece, cx, cy)) { - float px = ox + (piece.x + cx) * tileSize; + float px = ox + (piece.x + cx) * tileSize + pixelOffsetX; float py = oy + (piece.y + cy) * tileSize + pixelOffsetY; if (isGhost) { @@ -380,6 +389,28 @@ void GameRenderer::renderPlayingState( bool allowActivePieceRender = true; const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled(); + float activePiecePixelOffsetX = 0.0f; + if (allowActivePieceRender) { + if (smoothScrollEnabled && !game->isPaused()) { + const uint64_t pieceSeq = game->getCurrentPieceSequence(); + if (!s_activePieceSmooth.initialized || s_activePieceSmooth.sequence != pieceSeq) { + s_activePieceSmooth.sequence = pieceSeq; + s_activePieceSmooth.visualX = static_cast(game->current().x); + s_activePieceSmooth.initialized = true; + } + + const float targetX = static_cast(game->current().x); + constexpr float HORIZONTAL_SMOOTH_MS = 55.0f; + const float lerpFactor = std::clamp(sparkDeltaMs / HORIZONTAL_SMOOTH_MS, 0.0f, 1.0f); + s_activePieceSmooth.visualX = std::lerp(s_activePieceSmooth.visualX, targetX, lerpFactor); + activePiecePixelOffsetX = (s_activePieceSmooth.visualX - targetX) * finalBlockSize; + } else { + s_activePieceSmooth.sequence = game->getCurrentPieceSequence(); + s_activePieceSmooth.visualX = static_cast(game->current().x); + s_activePieceSmooth.initialized = true; + } + } + auto computeFallOffset = [&]() -> float { if (game->isPaused()) { return 0.0f; @@ -398,8 +429,8 @@ void GameRenderer::renderPlayingState( return progress * finalBlockSize; }; - float activePieceOffset = (!game->isPaused() && smoothScrollEnabled) ? computeFallOffset() : 0.0f; - if (activePieceOffset > 0.0f) { + float activePiecePixelOffsetY = (!game->isPaused() && smoothScrollEnabled) ? computeFallOffset() : 0.0f; + if (activePiecePixelOffsetY > 0.0f) { const auto& boardRef = game->boardRef(); const Game::Piece& piece = game->current(); float maxAllowed = finalBlockSize; @@ -430,7 +461,7 @@ void GameRenderer::renderPlayingState( maxAllowed = std::min(maxAllowed, cellLimit); } } - activePieceOffset = std::min(activePieceOffset, maxAllowed); + activePiecePixelOffsetY = std::min(activePiecePixelOffsetY, maxAllowed); } // Draw ghost piece (where current piece will land) @@ -468,7 +499,7 @@ void GameRenderer::renderPlayingState( // Draw the falling piece if (allowActivePieceRender) { - drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false, activePieceOffset); + drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false, activePiecePixelOffsetX, activePiecePixelOffsetY); } // Draw line clearing effects diff --git a/src/graphics/renderers/GameRenderer.h b/src/graphics/renderers/GameRenderer.h index 77e55a2..96422cf 100644 --- a/src/graphics/renderers/GameRenderer.h +++ b/src/graphics/renderers/GameRenderer.h @@ -50,7 +50,7 @@ public: private: // Helper functions for drawing game elements static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType); - static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false, float pixelOffsetY = 0.0f); + static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false, float pixelOffsetX = 0.0f, float pixelOffsetY = 0.0f); static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize); // Helper function for drawing rectangles From 55c40f0516b530abad0c8304610cbc7e8dada896 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sun, 30 Nov 2025 13:30:02 +0100 Subject: [PATCH 2/3] added helper menu --- CMakeLists.txt | 1 + settings.ini | 2 +- src/core/application/ApplicationManager.cpp | 62 ++++++++- src/core/application/ApplicationManager.h | 2 + src/graphics/ui/HelpOverlay.cpp | 133 ++++++++++++++++++++ src/graphics/ui/HelpOverlay.h | 16 +++ src/main.cpp | 72 ++++++++--- src/states/MenuState.cpp | 3 +- src/states/PlayingState.cpp | 3 +- src/states/State.h | 1 + src/ui/MenuWrappers.cpp | 3 +- 11 files changed, 272 insertions(+), 26 deletions(-) create mode 100644 src/graphics/ui/HelpOverlay.cpp create mode 100644 src/graphics/ui/HelpOverlay.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 18b9efc..b530b2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ add_executable(tetris src/graphics/effects/Starfield.cpp src/graphics/effects/Starfield3D.cpp src/graphics/ui/Font.cpp + src/graphics/ui/HelpOverlay.cpp src/graphics/renderers/GameRenderer.cpp src/audio/Audio.cpp src/gameplay/effects/LineEffect.cpp diff --git a/settings.ini b/settings.ini index 384ce2a..200523e 100644 --- a/settings.ini +++ b/settings.ini @@ -12,7 +12,7 @@ Sound=1 SmoothScroll=1 [Player] -Name=PLAYER +Name=GREGOR [Debug] Enabled=1 diff --git a/src/core/application/ApplicationManager.cpp b/src/core/application/ApplicationManager.cpp index fd282d0..c50bdd4 100644 --- a/src/core/application/ApplicationManager.cpp +++ b/src/core/application/ApplicationManager.cpp @@ -20,6 +20,7 @@ #include "../GlobalState.h" #include "../../graphics/renderers/RenderManager.h" #include "../../graphics/ui/Font.h" +#include "../../graphics/ui/HelpOverlay.h" #include "../../graphics/effects/Starfield3D.h" #include "../../graphics/effects/Starfield.h" #include "../../graphics/renderers/GameRenderer.h" @@ -351,9 +352,26 @@ bool ApplicationManager::initializeManagers() { consume = true; } - // N: Play a test sound effect - if (!consume && sc == SDL_SCANCODE_N) { - SoundEffectManager::instance().playSound("lets_go", 1.0f); + if (!consume && sc == SDL_SCANCODE_H) { + AppState currentState = m_stateManager ? m_stateManager->getState() : AppState::Loading; + if (currentState != AppState::Loading) { + m_showHelpOverlay = !m_showHelpOverlay; + if (currentState == AppState::Playing && m_game) { + if (m_showHelpOverlay) { + if (!m_game->isPaused()) { + m_game->setPaused(true); + m_helpOverlayPausedGame = true; + } else { + m_helpOverlayPausedGame = false; + } + } else if (m_helpOverlayPausedGame) { + m_game->setPaused(false); + m_helpOverlayPausedGame = false; + } + } else if (!m_showHelpOverlay) { + m_helpOverlayPausedGame = false; + } + } consume = true; } } @@ -370,6 +388,7 @@ bool ApplicationManager::initializeManagers() { m_inputManager->registerMouseButtonHandler([this](int button, bool pressed, float x, float y){ if (!m_stateManager) return; + if (m_showHelpOverlay) return; SDL_Event ev{}; ev.type = pressed ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP; ev.button.button = button; @@ -380,6 +399,7 @@ bool ApplicationManager::initializeManagers() { m_inputManager->registerMouseMotionHandler([this](float x, float y, float dx, float dy){ if (!m_stateManager) return; + if (m_showHelpOverlay) return; SDL_Event ev{}; ev.type = SDL_EVENT_MOUSE_MOTION; ev.motion.x = int(x); @@ -594,6 +614,7 @@ bool ApplicationManager::initializeGame() { m_stateContext.startLevelSelection = &m_startLevelSelection; m_stateContext.hoveredButton = &m_hoveredButton; m_stateContext.showSettingsPopup = &m_showSettingsPopup; + m_stateContext.showHelpOverlay = &m_showHelpOverlay; m_stateContext.showExitConfirmPopup = &m_showExitConfirmPopup; m_stateContext.exitPopupSelectedButton = &m_exitPopupSelectedButton; m_stateContext.playerName = &m_playerName; @@ -1336,6 +1357,41 @@ void ApplicationManager::setupStateHandlers() { m_stateManager->registerRenderHandler(AppState::Playing, debugOverlay); m_stateManager->registerRenderHandler(AppState::GameOver, debugOverlay); } + + auto helpOverlayRender = [this](RenderManager& renderer) { + if (!m_showHelpOverlay || !m_stateContext.pixelFont) { + return; + } + + SDL_Renderer* sdlRenderer = renderer.getSDLRenderer(); + if (!sdlRenderer) { + return; + } + + SDL_Rect logicalVP = renderer.getLogicalViewport(); + float logicalScale = renderer.getLogicalScale(); + SDL_SetRenderViewport(sdlRenderer, &logicalVP); + SDL_SetRenderScale(sdlRenderer, logicalScale, logicalScale); + HelpOverlay::Render( + sdlRenderer, + *m_stateContext.pixelFont, + static_cast(Config::Logical::WIDTH), + static_cast(Config::Logical::HEIGHT), + 0.0f, + 0.0f + ); + SDL_SetRenderViewport(sdlRenderer, nullptr); + SDL_SetRenderScale(sdlRenderer, 1.0f, 1.0f); + }; + + if (m_stateManager) { + m_stateManager->registerRenderHandler(AppState::Loading, helpOverlayRender); + m_stateManager->registerRenderHandler(AppState::Menu, helpOverlayRender); + m_stateManager->registerRenderHandler(AppState::Options, helpOverlayRender); + m_stateManager->registerRenderHandler(AppState::LevelSelector, helpOverlayRender); + m_stateManager->registerRenderHandler(AppState::Playing, helpOverlayRender); + m_stateManager->registerRenderHandler(AppState::GameOver, helpOverlayRender); + } } void ApplicationManager::processEvents() { diff --git a/src/core/application/ApplicationManager.h b/src/core/application/ApplicationManager.h index 386f31d..220b2a5 100644 --- a/src/core/application/ApplicationManager.h +++ b/src/core/application/ApplicationManager.h @@ -94,6 +94,7 @@ private: int m_startLevelSelection = 0; int m_hoveredButton = -1; bool m_showSettingsPopup = false; + bool m_showHelpOverlay = false; bool m_showExitConfirmPopup = false; int m_exitPopupSelectedButton = 1; // 0 = YES, 1 = NO bool m_isFullscreen = false; @@ -140,6 +141,7 @@ private: // Animation state float m_logoAnimCounter = 0.0f; + bool m_helpOverlayPausedGame = false; // Gameplay background (per-level) with fade, mirroring main.cpp behavior SDL_Texture* m_levelBackgroundTex = nullptr; diff --git a/src/graphics/ui/HelpOverlay.cpp b/src/graphics/ui/HelpOverlay.cpp new file mode 100644 index 0000000..b1469d6 --- /dev/null +++ b/src/graphics/ui/HelpOverlay.cpp @@ -0,0 +1,133 @@ +#include "HelpOverlay.h" +#include "Font.h" +#include +#include + +namespace { +struct ShortcutEntry { + const char* combo; + const char* description; +}; + +void drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color color) { + SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); + SDL_FRect rect{x, y, w, h}; + SDL_RenderFillRect(renderer, &rect); +} + +float fitScale(FontAtlas& font, const char* text, float initialScale, float maxWidth) { + int textW = 0, textH = 0; + float scale = initialScale; + font.measure(text, scale, textW, textH); + if (textW > maxWidth && textW > 0) { + float factor = maxWidth / static_cast(textW); + scale = std::max(0.65f, scale * factor); + } + return scale; +} +} + +namespace HelpOverlay { + +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"}, + {"ESC", "Back / cancel current popup"}, + {"F11 or ALT+ENTER", "Toggle fullscreen"}, + {"M", "Mute or unmute music"}, + {"S", "Toggle sound effects"} + }}; + + const std::array menuShortcuts{{ + {"ARROW KEYS", "Navigate menu buttons"}, + {"ENTER / SPACE", "Activate highlighted action"} + }}; + + const std::array gameplayShortcuts{{ + {"LEFT / RIGHT", "Move active piece"}, + {"DOWN", "Soft drop (faster fall)"}, + {"SPACE", "Hard drop / instant lock"}, + {"UP", "Rotate clockwise"}, + {"X", "Rotate counter-clockwise"}, + {"P", "Pause or resume"}, + {"ESC", "Open exit confirmation"} + }}; + + const float boxW = logicalWidth * 0.7f; + const float boxH = logicalHeight * 0.55f; + const float boxX = offsetX + (logicalWidth - boxW) * 0.5f; + const float boxY = offsetY + (logicalHeight - boxH) * 0.5f; + + drawRect(renderer, boxX - 6.0f, boxY - 6.0f, boxW + 12.0f, boxH + 12.0f, {70, 90, 150, 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}); + + const float titleScale = 1.7f; + font.draw(renderer, boxX + 28.0f, boxY + 24.0f, "HELP & SHORTCUTS", titleScale, {255, 220, 0, 255}); + + const float contentPadding = 32.0f; + const float columnGap = 30.0f; + const float columnWidth = (boxW - contentPadding * 2.0f - columnGap) * 0.5f; + const float leftColumnX = boxX + contentPadding; + const float rightColumnX = leftColumnX + columnWidth + columnGap; + 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; + + 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; + for (const auto& entry : entries) { + font.draw(renderer, startX, cursorY, entry.combo, comboScale, {255, 255, 255, 255}); + 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; + } + cursorY += sectionSpacing; + }; + + float leftCursorY = boxY + 80.0f; + float rightCursorY = boxY + 80.0f; + drawSection(leftColumnX, leftCursorY, "GENERAL", generalShortcuts); + drawSection(leftColumnX, leftCursorY, "MENUS", menuShortcuts); + drawSection(rightColumnX, rightCursorY, "GAMEPLAY", gameplayShortcuts); + + SDL_FRect footerRect{ + boxX + contentPadding, + boxY + boxH - contentPadding - footerHeight, + boxW - contentPadding * 2.0f, + footerHeight + }; + drawRect(renderer, footerRect.x, footerRect.y, footerRect.w, footerRect.h, {24, 30, 50, 255}); + SDL_SetRenderDrawColor(renderer, 90, 110, 170, 255); + SDL_RenderRect(renderer, &footerRect); + + const char* closeLabel = "PRESS H 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); + float closeX = footerRect.x + (footerRect.w - static_cast(closeW)) * 0.5f; + float closeY = footerRect.y + (footerRect.h - static_cast(closeH)) * 0.5f; + font.draw(renderer, closeX, closeY, closeLabel, closeScale, {215, 220, 240, 255}); +} + +} // namespace HelpOverlay diff --git a/src/graphics/ui/HelpOverlay.h b/src/graphics/ui/HelpOverlay.h new file mode 100644 index 0000000..1de9b68 --- /dev/null +++ b/src/graphics/ui/HelpOverlay.h @@ -0,0 +1,16 @@ +#pragma once +#include + +class FontAtlas; + +namespace HelpOverlay { + // Draws the help popup contents inside the logical coordinate space. + // Optional offsets allow aligning the box when the logical canvas is letterboxed. + void Render( + SDL_Renderer* renderer, + FontAtlas& font, + float logicalWidth, + float logicalHeight, + float offsetX = 0.0f, + float offsetY = 0.0f); +} diff --git a/src/main.cpp b/src/main.cpp index 5c38567..aaf215e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include "graphics/effects/Starfield.h" #include "graphics/effects/Starfield3D.h" #include "graphics/ui/Font.h" +#include "graphics/ui/HelpOverlay.h" #include "gameplay/effects/LineEffect.h" #include "states/State.h" #include "states/LoadingState.h" @@ -487,10 +488,10 @@ static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musi // Instructions font.draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, {200, 200, 220, 255}); font.draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, {200, 200, 220, 255}); - font.draw(renderer, popupX + 20, popupY + 190, "N = PLAY LETS_GO", 1.0f, {200, 200, 220, 255}); - font.draw(renderer, popupX + 20, popupY + 210, "ESC = CLOSE", 1.0f, {200, 200, 220, 255}); + font.draw(renderer, popupX + 20, popupY + 190, "ESC = CLOSE", 1.0f, {200, 200, 220, 255}); } + // ----------------------------------------------------------------------------- // Starfield effect for background // ----------------------------------------------------------------------------- @@ -504,12 +505,14 @@ static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musi // ----------------------------------------------------------------------------- static double logoAnimCounter = 0.0; static bool showSettingsPopup = false; +static bool showHelpOverlay = false; static bool showExitConfirmPopup = false; static int exitPopupSelectedButton = 1; // 0 = YES, 1 = NO static bool musicEnabled = true; static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings static bool isNewHighScore = false; static std::string playerName = ""; +static bool helpOverlayPausedGame = false; // ----------------------------------------------------------------------------- // Tetris Block Fireworks for intro animation (block particles) @@ -904,6 +907,7 @@ int main(int, char **) ctx.startLevelSelection = &startLevelSelection; ctx.hoveredButton = &hoveredButton; ctx.showSettingsPopup = &showSettingsPopup; + ctx.showHelpOverlay = &showHelpOverlay; ctx.showExitConfirmPopup = &showExitConfirmPopup; ctx.exitPopupSelectedButton = &exitPopupSelectedButton; ctx.gameplayCountdownActive = &gameplayCountdownActive; @@ -1027,12 +1031,22 @@ int main(int, char **) running = false; else { // Route event to state manager handlers for per-state logic - stateMgr.handleEvent(e); - // Keep the local `state` variable in sync with StateManager in case - // a state handler requested a transition (handlers may call - // stateMgr.setState()). Many branches below rely on the local - // `state` variable, so update it immediately after handling. - state = stateMgr.getState(); + const bool isUserInputEvent = + e.type == SDL_EVENT_KEY_DOWN || + e.type == SDL_EVENT_KEY_UP || + e.type == SDL_EVENT_TEXT_INPUT || + e.type == SDL_EVENT_MOUSE_BUTTON_DOWN || + e.type == SDL_EVENT_MOUSE_BUTTON_UP || + e.type == SDL_EVENT_MOUSE_MOTION; + + if (!(showHelpOverlay && isUserInputEvent)) { + stateMgr.handleEvent(e); + // Keep the local `state` variable in sync with StateManager in case + // a state handler requested a transition (handlers may call + // stateMgr.setState()). Many branches below rely on the local + // `state` variable, so update it immediately after handling. + state = stateMgr.getState(); + } // Global key toggles (applies regardless of state) if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { @@ -1048,11 +1062,23 @@ int main(int, char **) SoundEffectManager::instance().setEnabled(!SoundEffectManager::instance().isEnabled()); Settings::instance().setSoundEnabled(SoundEffectManager::instance().isEnabled()); } - if (e.key.scancode == SDL_SCANCODE_N) + if (e.key.scancode == SDL_SCANCODE_H && state != AppState::Loading) { - // Manually trigger a random voice line for quick testing - if (!allVoiceSounds.empty()) { - SoundEffectManager::instance().playRandomSound(allVoiceSounds, 1.0f); + showHelpOverlay = !showHelpOverlay; + if (state == AppState::Playing) { + if (showHelpOverlay) { + if (!game.isPaused()) { + game.setPaused(true); + helpOverlayPausedGame = true; + } else { + helpOverlayPausedGame = false; + } + } else if (helpOverlayPausedGame) { + game.setPaused(false); + helpOverlayPausedGame = false; + } + } else if (!showHelpOverlay) { + helpOverlayPausedGame = false; } } if (e.key.key == SDLK_F11 || (e.key.key == SDLK_RETURN && (e.key.mod & SDL_KMOD_ALT))) @@ -1064,13 +1090,13 @@ int main(int, char **) } // Text input for high score - if (state == AppState::GameOver && isNewHighScore && e.type == SDL_EVENT_TEXT_INPUT) { + if (!showHelpOverlay && state == AppState::GameOver && isNewHighScore && e.type == SDL_EVENT_TEXT_INPUT) { if (playerName.length() < 12) { playerName += e.text.text; } } - if (state == AppState::GameOver && e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { + if (!showHelpOverlay && state == AppState::GameOver && e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { if (isNewHighScore) { if (e.key.scancode == SDL_SCANCODE_BACKSPACE && !playerName.empty()) { playerName.pop_back(); @@ -1096,7 +1122,7 @@ int main(int, char **) } // Mouse handling remains in main loop for UI interactions - if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) + if (!showHelpOverlay && e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { float mx = (float)e.button.x, my = (float)e.button.y; if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) @@ -1227,7 +1253,7 @@ int main(int, char **) } } } - else if (e.type == SDL_EVENT_MOUSE_MOTION) + else if (!showHelpOverlay && e.type == SDL_EVENT_MOUSE_MOTION) { float mx = (float)e.motion.x, my = (float)e.motion.y; if (mx >= logicalVP.x && my >= logicalVP.y && mx <= logicalVP.x + logicalVP.w && my <= logicalVP.y + logicalVP.h) @@ -1886,6 +1912,20 @@ int main(int, char **) SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); } + if (showHelpOverlay) { + SDL_SetRenderViewport(renderer, &logicalVP); + SDL_SetRenderScale(renderer, logicalScale, logicalScale); + float contentOffsetX = 0.0f; + float contentOffsetY = 0.0f; + if (logicalScale > 0.0f) { + float scaledW = LOGICAL_W * logicalScale; + float scaledH = LOGICAL_H * logicalScale; + contentOffsetX = (winW - scaledW) * 0.5f / logicalScale; + contentOffsetY = (winH - scaledH) * 0.5f / logicalScale; + } + HelpOverlay::Render(renderer, pixelFont, LOGICAL_W, LOGICAL_H, contentOffsetX, contentOffsetY); + } + SDL_RenderPresent(renderer); SDL_SetRenderScale(renderer, 1.f, 1.f); } diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index 7344229..40572b7 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -465,8 +465,7 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi ctx.font->draw(renderer, popupX + 140, popupY + 100, "ON", 1.5f, SDL_Color{0,255,0,255}); ctx.font->draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, SDL_Color{200,200,220,255}); ctx.font->draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, SDL_Color{200,200,220,255}); - ctx.font->draw(renderer, popupX + 20, popupY + 190, "N = PLAY LETS_GO", 1.0f, SDL_Color{200,200,220,255}); - ctx.font->draw(renderer, popupX + 20, popupY + 210, "ESC = CLOSE", 1.0f, SDL_Color{200,200,220,255}); + ctx.font->draw(renderer, popupX + 20, popupY + 190, "ESC = CLOSE", 1.0f, SDL_Color{200,200,220,255}); } // Trace exit { diff --git a/src/states/PlayingState.cpp b/src/states/PlayingState.cpp index 35e7a0d..01acecd 100644 --- a/src/states/PlayingState.cpp +++ b/src/states/PlayingState.cpp @@ -104,8 +104,7 @@ void PlayingState::handleEvent(const SDL_Event& e) { // Tetris controls (only when not paused) if (!ctx.game->isPaused()) { // Rotation (still event-based for precise timing) - if (e.key.scancode == SDL_SCANCODE_UP || e.key.scancode == SDL_SCANCODE_W || - e.key.scancode == SDL_SCANCODE_Z) { + if (e.key.scancode == SDL_SCANCODE_UP) { ctx.game->rotate(1); // Clockwise rotation return; } diff --git a/src/states/State.h b/src/states/State.h index 87601a8..68bee3c 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -49,6 +49,7 @@ struct StateContext { int* hoveredButton = nullptr; // Menu popups (exposed from main) bool* showSettingsPopup = nullptr; + bool* showHelpOverlay = nullptr; bool* showExitConfirmPopup = nullptr; // If true, show "Exit game?" confirmation while playing int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default) bool* gameplayCountdownActive = nullptr; // True if start-of-game countdown is running diff --git a/src/ui/MenuWrappers.cpp b/src/ui/MenuWrappers.cpp index 62a54d7..cb23a98 100644 --- a/src/ui/MenuWrappers.cpp +++ b/src/ui/MenuWrappers.cpp @@ -83,6 +83,5 @@ void menu_drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musicE font.draw(renderer, popupX + 140, popupY + 100, sfxOn ? "ON" : "OFF", 1.5f, sfxOn ? SDL_Color{0,255,0,255} : SDL_Color{255,0,0,255}); font.draw(renderer, popupX + 20, popupY + 150, "M = TOGGLE MUSIC", 1.0f, SDL_Color{200,200,220,255}); font.draw(renderer, popupX + 20, popupY + 170, "S = TOGGLE SOUND FX", 1.0f, SDL_Color{200,200,220,255}); - font.draw(renderer, popupX + 20, popupY + 190, "N = PLAY LETS_GO", 1.0f, SDL_Color{200,200,220,255}); - font.draw(renderer, popupX + 20, popupY + 210, "ESC = CLOSE", 1.0f, SDL_Color{200,200,220,255}); + font.draw(renderer, popupX + 20, popupY + 190, "ESC = CLOSE", 1.0f, SDL_Color{200,200,220,255}); } From 9ea018333970d91bf2d1a81575625f31523d24ab Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Sun, 30 Nov 2025 14:00:32 +0100 Subject: [PATCH 3/3] added stars to gameplay grid --- src/graphics/GameRenderer.cpp | 10 ++++++++++ src/graphics/Starfield3D.cpp | 15 ++++++++++----- src/graphics/Starfield3D.h | 4 ++-- src/graphics/effects/Starfield3D.cpp | 15 ++++++++++----- src/graphics/effects/Starfield3D.h | 4 ++-- src/graphics/renderers/GameRenderer.cpp | 20 ++++++++++++++++++++ 6 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/graphics/GameRenderer.cpp b/src/graphics/GameRenderer.cpp index 13243e4..8cb7552 100644 --- a/src/graphics/GameRenderer.cpp +++ b/src/graphics/GameRenderer.cpp @@ -6,6 +6,10 @@ #include #include #include +#include +#include + +namespace { // Color constants (copied from main.cpp) static const SDL_Color COLORS[] = { @@ -217,6 +221,12 @@ void GameRenderer::renderPlayingState( float lineY = gridY + y * finalBlockSize; SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY); } + + Uint64 nowTicks = SDL_GetTicks(); + float deltaSeconds = (g_lastSparkTick == 0) ? (1.0f / 60.0f) : static_cast(nowTicks - g_lastSparkTick) / 1000.0f; + g_lastSparkTick = nowTicks; + updateSparks(std::max(0.0f, deltaSeconds)); + drawSparks(renderer, gridX, gridY, finalBlockSize); // Draw block statistics panel border drawRectWithOffset(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255}); diff --git a/src/graphics/Starfield3D.cpp b/src/graphics/Starfield3D.cpp index 3fe7740..fa038c4 100644 --- a/src/graphics/Starfield3D.cpp +++ b/src/graphics/Starfield3D.cpp @@ -137,16 +137,16 @@ void Starfield3D::update(float deltaTime) { } } -void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, int type) { - const SDL_Color& color = STAR_COLORS[type % COLOR_COUNT]; - SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); +void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, SDL_Color color, float alphaScale) { + Uint8 alpha = static_cast(std::clamp(color.a * alphaScale, 0.0f, 255.0f)); + SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, alpha); // Draw star as a small rectangle (1x1 pixel) SDL_FRect rect{x, y, 1.0f, 1.0f}; SDL_RenderFillRect(renderer, &rect); } -void Starfield3D::draw(SDL_Renderer* renderer) { +void Starfield3D::draw(SDL_Renderer* renderer, float offsetX, float offsetY, float alphaScale, bool grayscale) { for (const Star3D& star : stars) { // Calculate perspective projection factor const float k = DEPTH_FACTOR / star.z; @@ -158,7 +158,12 @@ void Starfield3D::draw(SDL_Renderer* renderer) { // Only draw stars that are within the viewport if (px >= 0.0f && px <= static_cast(width) && py >= 0.0f && py <= static_cast(height)) { - drawStar(renderer, px, py, star.type); + SDL_Color baseColor = STAR_COLORS[star.type % COLOR_COUNT]; + if (grayscale) { + Uint8 gray = static_cast(0.299f * baseColor.r + 0.587f * baseColor.g + 0.114f * baseColor.b); + baseColor.r = baseColor.g = baseColor.b = gray; + } + drawStar(renderer, px + offsetX, py + offsetY, baseColor, alphaScale); } } } diff --git a/src/graphics/Starfield3D.h b/src/graphics/Starfield3D.h index 7dfd5c9..3e03189 100644 --- a/src/graphics/Starfield3D.h +++ b/src/graphics/Starfield3D.h @@ -13,7 +13,7 @@ public: void init(int width, int height, int starCount = 160); void update(float deltaTime); - void draw(SDL_Renderer* renderer); + void draw(SDL_Renderer* renderer, float offsetX = 0.0f, float offsetY = 0.0f, float alphaScale = 1.0f, bool grayscale = false); void resize(int width, int height); private: @@ -32,7 +32,7 @@ private: void setRandomDirection(Star3D& star); float randomFloat(float min, float max); int randomRange(int min, int max); - void drawStar(SDL_Renderer* renderer, float x, float y, int type); + void drawStar(SDL_Renderer* renderer, float x, float y, SDL_Color color, float alphaScale); std::vector stars; int width{0}, height{0}; diff --git a/src/graphics/effects/Starfield3D.cpp b/src/graphics/effects/Starfield3D.cpp index 3fe7740..fa038c4 100644 --- a/src/graphics/effects/Starfield3D.cpp +++ b/src/graphics/effects/Starfield3D.cpp @@ -137,16 +137,16 @@ void Starfield3D::update(float deltaTime) { } } -void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, int type) { - const SDL_Color& color = STAR_COLORS[type % COLOR_COUNT]; - SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); +void Starfield3D::drawStar(SDL_Renderer* renderer, float x, float y, SDL_Color color, float alphaScale) { + Uint8 alpha = static_cast(std::clamp(color.a * alphaScale, 0.0f, 255.0f)); + SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, alpha); // Draw star as a small rectangle (1x1 pixel) SDL_FRect rect{x, y, 1.0f, 1.0f}; SDL_RenderFillRect(renderer, &rect); } -void Starfield3D::draw(SDL_Renderer* renderer) { +void Starfield3D::draw(SDL_Renderer* renderer, float offsetX, float offsetY, float alphaScale, bool grayscale) { for (const Star3D& star : stars) { // Calculate perspective projection factor const float k = DEPTH_FACTOR / star.z; @@ -158,7 +158,12 @@ void Starfield3D::draw(SDL_Renderer* renderer) { // Only draw stars that are within the viewport if (px >= 0.0f && px <= static_cast(width) && py >= 0.0f && py <= static_cast(height)) { - drawStar(renderer, px, py, star.type); + SDL_Color baseColor = STAR_COLORS[star.type % COLOR_COUNT]; + if (grayscale) { + Uint8 gray = static_cast(0.299f * baseColor.r + 0.587f * baseColor.g + 0.114f * baseColor.b); + baseColor.r = baseColor.g = baseColor.b = gray; + } + drawStar(renderer, px + offsetX, py + offsetY, baseColor, alphaScale); } } } diff --git a/src/graphics/effects/Starfield3D.h b/src/graphics/effects/Starfield3D.h index 7dfd5c9..3e03189 100644 --- a/src/graphics/effects/Starfield3D.h +++ b/src/graphics/effects/Starfield3D.h @@ -13,7 +13,7 @@ public: void init(int width, int height, int starCount = 160); void update(float deltaTime); - void draw(SDL_Renderer* renderer); + void draw(SDL_Renderer* renderer, float offsetX = 0.0f, float offsetY = 0.0f, float alphaScale = 1.0f, bool grayscale = false); void resize(int width, int height); private: @@ -32,7 +32,7 @@ private: void setRandomDirection(Star3D& star); float randomFloat(float min, float max); int randomRange(int min, int max); - void drawStar(SDL_Renderer* renderer, float x, float y, int type); + void drawStar(SDL_Renderer* renderer, float x, float y, SDL_Color color, float alphaScale); std::vector stars; int width{0}, height{0}; diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 52bdd9d..6fb9fb5 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -10,6 +10,7 @@ #include #include #include "../../core/Settings.h" +#include "../../graphics/effects/Starfield3D.h" namespace { struct ImpactSpark { @@ -30,6 +31,9 @@ struct ActivePieceSmoothState { }; ActivePieceSmoothState s_activePieceSmooth; + +Starfield3D s_inGridStarfield; +bool s_starfieldInitialized = false; } // Color constants (copied from main.cpp) @@ -250,6 +254,22 @@ void GameRenderer::renderPlayingState( float lineY = gridY + y * finalBlockSize; SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY); } + + if (!s_starfieldInitialized) { + s_inGridStarfield.init(static_cast(GRID_W), static_cast(GRID_H), 180); + s_starfieldInitialized = true; + } else { + s_inGridStarfield.resize(static_cast(GRID_W), static_cast(GRID_H)); + } + + const float deltaSeconds = std::clamp(static_cast(sparkDeltaMs) / 1000.0f, 0.0f, 0.033f); + s_inGridStarfield.update(deltaSeconds); + + SDL_BlendMode oldBlend = SDL_BLENDMODE_NONE; + SDL_GetRenderDrawBlendMode(renderer, &oldBlend); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + s_inGridStarfield.draw(renderer, gridX, gridY, 0.22f, true); + SDL_SetRenderDrawBlendMode(renderer, oldBlend); // Draw block statistics panel border drawRectWithOffset(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255});