diff --git a/src/graphics/Font.cpp b/src/graphics/Font.cpp index 145da63..6520720 100644 --- a/src/graphics/Font.cpp +++ b/src/graphics/Font.cpp @@ -19,3 +19,18 @@ void FontAtlas::draw(SDL_Renderer* r, float x, float y, const std::string& text, if (tex) { SDL_FRect dst{ x, y, (float)surf->w, (float)surf->h }; SDL_RenderTexture(r, tex, nullptr, &dst); SDL_DestroyTexture(tex); } SDL_DestroySurface(surf); } + +void FontAtlas::measure(const std::string& text, float scale, int& outW, int& outH) { + outW = 0; outH = 0; + if (scale <= 0) return; + int pt = int(baseSize * scale); + if (pt < 1) pt = 1; + TTF_Font* f = getSized(pt); + if (!f) return; + // Use render-to-surface measurement to avoid dependency on specific TTF_* measurement API variants + SDL_Color dummy = {255,255,255,255}; + SDL_Surface* surf = TTF_RenderText_Blended(f, text.c_str(), text.length(), dummy); + if (!surf) return; + outW = surf->w; outH = surf->h; + SDL_DestroySurface(surf); +} diff --git a/src/graphics/Font.h b/src/graphics/Font.h index ba4b2a9..3ff2846 100644 --- a/src/graphics/Font.h +++ b/src/graphics/Font.h @@ -10,6 +10,8 @@ public: bool init(const std::string& path, int basePt); void shutdown(); void draw(SDL_Renderer* r, float x, float y, const std::string& text, float scale, SDL_Color color); + // Measure rendered text size in pixels for a given scale + void measure(const std::string& text, float scale, int& outW, int& outH); private: std::string fontPath; int baseSize{24}; diff --git a/src/main.cpp b/src/main.cpp index 25989c2..45d677c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -71,6 +71,8 @@ static void drawRect(SDL_Renderer *r, float x, float y, float w, float h, SDL_Co SDL_RenderFillRect(r, &fr); } +// ...existing code... + // ----------------------------------------------------------------------------- // Enhanced Button Drawing // ----------------------------------------------------------------------------- @@ -340,6 +342,7 @@ static void drawSettingsPopup(SDL_Renderer* renderer, FontAtlas& font, bool musi static double logoAnimCounter = 0.0; static bool showLevelPopup = false; static bool showSettingsPopup = false; +static bool showExitConfirmPopup = false; static bool musicEnabled = true; static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings @@ -707,6 +710,8 @@ int main(int, char **) // Prepare shared context for states StateContext ctx{}; + // Allow states to access the state manager for transitions + ctx.stateManager = &stateMgr; ctx.game = &game; ctx.scores = &scores; ctx.starfield = &starfield; @@ -725,6 +730,7 @@ int main(int, char **) ctx.hoveredButton = &hoveredButton; ctx.showLevelPopup = &showLevelPopup; ctx.showSettingsPopup = &showSettingsPopup; + ctx.showExitConfirmPopup = &showExitConfirmPopup; // Instantiate state objects auto loadingState = std::make_unique(ctx); @@ -899,6 +905,48 @@ int main(int, char **) state = AppState::Menu; stateMgr.setState(state); } + else if (state == AppState::Playing && showExitConfirmPopup) { + // Convert mouse to logical coordinates and to content-local coords + float lx = (mx - logicalVP.x) / logicalScale; + float ly = (my - logicalVP.y) / logicalScale; + // Compute content offsets (same as in render path) + float contentW = LOGICAL_W * logicalScale; + float contentH = LOGICAL_H * logicalScale; + float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; + float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; + // Map to content-local logical coords (what drawing code uses) + float localX = lx - contentOffsetX; + float localY = ly - contentOffsetY; + + // Popup rect in logical coordinates (content-local) + float popupW = 420, popupH = 180; + float popupX = (LOGICAL_W - popupW) / 2; + float popupY = (LOGICAL_H - popupH) / 2; + + if (localX >= popupX && localX <= popupX + popupW && localY >= popupY && localY <= popupY + popupH) { + // Inside popup: two buttons Yes / No + float btnW = 140, btnH = 46; + float yesX = popupX + popupW * 0.25f - btnW/2.0f; + float noX = popupX + popupW * 0.75f - btnW/2.0f; + float btnY = popupY + popupH - 60; + if (localX >= yesX && localX <= yesX + btnW && localY >= btnY && localY <= btnY + btnH) { + // Yes -> go back to menu + showExitConfirmPopup = false; + game.reset(startLevelSelection); + state = AppState::Menu; + stateMgr.setState(state); + } + else if (localX >= noX && localX <= noX + btnW && localY >= btnY && localY <= btnY + btnH) { + // No -> close popup and resume + showExitConfirmPopup = false; + game.setPaused(false); + } + } else { + // Click outside popup: cancel + showExitConfirmPopup = false; + game.setPaused(false); + } + } } } else if (e.type == SDL_EVENT_MOUSE_MOTION) @@ -1608,8 +1656,8 @@ int main(int, char **) drawSmallPiece(renderer, blocksTex, game.held().type, statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); } - // Pause overlay - if (game.isPaused()) { + // Pause overlay: don't draw pause UI when the exit-confirm popup is showing + if (game.isPaused() && !showExitConfirmPopup) { // Semi-transparent overlay SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); SDL_FRect pauseOverlay{0, 0, LOGICAL_W, LOGICAL_H}; @@ -1620,6 +1668,66 @@ int main(int, char **) pixelFont.draw(renderer, LOGICAL_W * 0.5f - 120, LOGICAL_H * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255}); } + // Exit confirmation popup (modal) + if (showExitConfirmPopup) { + // Compute content offsets for consistent placement across window sizes + float contentW = LOGICAL_W * logicalScale; + float contentH = LOGICAL_H * logicalScale; + float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; + float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; + + float popupW = 420, popupH = 180; + float popupX = (LOGICAL_W - popupW) / 2; + float popupY = (LOGICAL_H - popupH) / 2; + + // Dim entire window (use window coordinates so it always covers 100% of the target) + SDL_SetRenderViewport(renderer, nullptr); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200); + SDL_FRect fullWin{0.f, 0.f, (float)winW, (float)winH}; + SDL_RenderFillRect(renderer, &fullWin); + // Restore logical viewport for drawing content-local popup + SDL_SetRenderViewport(renderer, &logicalVP); + + // Draw popup box (drawRect will apply contentOffset internally) + drawRect(popupX - 4, popupY - 4, popupW + 8, popupH + 8, {60, 70, 90, 255}); + drawRect(popupX, popupY, popupW, popupH, {20, 22, 28, 240}); + + // Center title and body text inside popup (use pixelFont for retro P2 font) + const std::string title = "Exit game?"; + const std::string line1 = "Are you sure you want to"; + const std::string line2 = "leave the current game?"; + + int wTitle=0,hTitle=0; pixelFont.measure( title, 1.6f, wTitle, hTitle); + int wL1=0,hL1=0; pixelFont.measure( line1, 0.9f, wL1, hL1); + int wL2=0,hL2=0; pixelFont.measure( line2, 0.9f, wL2, hL2); + + float titleX = popupX + (popupW - (float)wTitle) * 0.5f; + float l1X = popupX + (popupW - (float)wL1) * 0.5f; + float l2X = popupX + (popupW - (float)wL2) * 0.5f; + + pixelFont.draw(renderer, titleX + contentOffsetX, popupY + contentOffsetY + 20, title, 1.6f, {255, 220, 0, 255}); + pixelFont.draw(renderer, l1X + contentOffsetX, popupY + contentOffsetY + 60, line1, 0.9f, SDL_Color{220,220,230,255}); + pixelFont.draw(renderer, l2X + contentOffsetX, popupY + contentOffsetY + 84, line2, 0.9f, SDL_Color{220,220,230,255}); + + // Buttons (center labels inside buttons) - use pixelFont for labels + float btnW = 140, btnH = 46; + float yesX = popupX + popupW * 0.25f - btnW/2.0f; + float noX = popupX + popupW * 0.75f - btnW/2.0f; + float btnY = popupY + popupH - 60; + + drawRect(yesX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255}); + drawRect(yesX, btnY, btnW, btnH, {200, 60, 60, 255}); + const std::string yes = "YES"; + int wy=0,hy=0; pixelFont.measure( yes, 1.0f, wy, hy); + pixelFont.draw(renderer, yesX + (btnW - (float)wy) * 0.5f + contentOffsetX, btnY + (btnH - (float)hy) * 0.5f + contentOffsetY, yes, 1.0f, {255,255,255,255}); + + drawRect(noX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255}); + drawRect(noX, btnY, btnW, btnH, {80, 140, 80, 255}); + const std::string no = "NO"; + int wn=0,hn=0; pixelFont.measure( no, 1.0f, wn, hn); + pixelFont.draw(renderer, noX + (btnW - (float)wn) * 0.5f + contentOffsetX, btnY + (btnH - (float)hn) * 0.5f + contentOffsetY, no, 1.0f, {255,255,255,255}); + } + // Controls hint at bottom font.draw(renderer, 20, LOGICAL_H - 30, "ARROWS=Move Z/X=Rotate C=Hold SPACE=Drop P=Pause ESC=Menu", 1.0f, {150, 150, 170, 255}); } diff --git a/src/states/PlayingState.cpp b/src/states/PlayingState.cpp index 91086d7..571e00c 100644 --- a/src/states/PlayingState.cpp +++ b/src/states/PlayingState.cpp @@ -1,4 +1,5 @@ #include "PlayingState.h" +#include "core/StateManager.h" #include "gameplay/Game.h" #include "gameplay/LineEffect.h" #include "persistence/Scores.h" @@ -14,14 +15,45 @@ void PlayingState::onExit() { } void PlayingState::handleEvent(const SDL_Event& e) { - // We keep short-circuited input here; main still handles mouse UI + // We keep short-circuited input here; main still owns mouse UI if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { if (!ctx.game) return; // Pause toggle (P) if (e.key.scancode == SDL_SCANCODE_P) { bool paused = ctx.game->isPaused(); ctx.game->setPaused(!paused); + return; } + + // If exit-confirm popup is visible, handle shortcuts here + if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) { + // Confirm with Enter (main or keypad) + if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) { + *ctx.showExitConfirmPopup = false; + // Reset game and return to menu + ctx.game->reset(false); + if (ctx.stateManager) ctx.stateManager->setState(AppState::Menu); + return; + } + // Cancel with Esc + if (e.key.scancode == SDL_SCANCODE_ESCAPE) { + *ctx.showExitConfirmPopup = false; + ctx.game->setPaused(false); + return; + } + // While modal is open, suppress other gameplay keys + return; + } + + // ESC key - open confirmation popup + if (e.key.scancode == SDL_SCANCODE_ESCAPE) { + if (ctx.showExitConfirmPopup) { + if (ctx.game) ctx.game->setPaused(true); + *ctx.showExitConfirmPopup = true; + } + return; + } + // Other gameplay keys already registered by main's Playing handler for now } } diff --git a/src/states/State.h b/src/states/State.h index f5a30dd..f75c924 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -12,6 +12,10 @@ class Starfield3D; class FontAtlas; class LineEffect; +// Forward declare StateManager so StateContext can hold a pointer without +// including the StateManager header here. +class StateManager; + // Shared context passed to states so they can access common resources struct StateContext { // Core subsystems (may be null if not available) @@ -41,6 +45,9 @@ struct StateContext { // Menu popups (exposed from main) bool* showLevelPopup = nullptr; bool* showSettingsPopup = nullptr; + bool* showExitConfirmPopup = nullptr; // If true, show "Exit game?" confirmation while playing + // Pointer to the application's StateManager so states can request transitions + StateManager* stateManager = nullptr; }; class State {