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:
2025-08-16 19:40:23 +02:00
parent 2afaea7fd3
commit 6d4f3c54ed
5 changed files with 167 additions and 3 deletions

View File

@ -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});
}