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:
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});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user