Add exit-confirm modal (fullscreen dim, centered P2 text) and keyboard shortcuts
Add an in-game exit confirmation modal for Playing state: ESC opens modal and pauses the game; YES resets and returns to Menu; NO hides modal and resumes. Draw a full-window translucent dim background (reset viewport) so overlay covers any window size / fullscreen. Use PressStart2P (pixel P2) font for all modal text and center title/body/button labels using measured text widths. Add FontAtlas::measure(...) to accurately measure text sizes (used for proper centering). Ensure popup rendering and mouse hit-testing use the same logical/content-local coordinate math so visuals and clicks align. Add keyboard shortcuts for modal (Enter = confirm, Esc = cancel) and suppress other gameplay input while modal is active. Add helper scripts for debug build+run: build-debug-and-run.ps1 and build-debug-and-run.bat. Minor fixes to related rendering & state wiring; verified Debug build completes and modal behavior in runtime.
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
||||
@ -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};
|
||||
|
||||
112
src/main.cpp
112
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<LoadingState>(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});
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user