#include "GameRenderer.h" #include "../gameplay/Game.h" #include "../graphics/Font.h" #include "../gameplay/LineEffect.h" #include #include #include #include #include #include namespace { // Color constants (copied from main.cpp) static const SDL_Color COLORS[] = { {0, 0, 0, 255}, // 0: BLACK (empty) {0, 255, 255, 255}, // 1: I-piece - cyan {255, 255, 0, 255}, // 2: O-piece - yellow {128, 0, 128, 255}, // 3: T-piece - purple {0, 255, 0, 255}, // 4: S-piece - green {255, 0, 0, 255}, // 5: Z-piece - red {0, 0, 255, 255}, // 6: J-piece - blue {255, 165, 0, 255} // 7: L-piece - orange }; void GameRenderer::drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c) { SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); SDL_FRect fr{x, y, w, h}; SDL_RenderFillRect(renderer, &fr); } void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) { if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) { // Fallback to colored rectangle if texture isn't available SDL_Color color = (blockType >= 0 && blockType < PIECE_COUNT) ? COLORS[blockType + 1] : SDL_Color{128, 128, 128, 255}; drawRect(renderer, x, y, size-1, size-1, color); return; } // JavaScript uses: sx = type * spriteSize, sy = 0, with 2px padding // Each sprite is 90px wide in the horizontal sprite sheet const int SPRITE_SIZE = 90; float srcX = blockType * SPRITE_SIZE + 2; // Add 2px padding like JS float srcY = 2; // Add 2px padding from top like JS float srcW = SPRITE_SIZE - 4; // Subtract 4px total padding like JS float srcH = SPRITE_SIZE - 4; // Subtract 4px total padding like JS SDL_FRect srcRect = {srcX, srcY, srcW, srcH}; SDL_FRect dstRect = {x, y, size, size}; 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) { 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 py = oy + (piece.y + cy) * tileSize; if (isGhost) { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); // Draw ghost piece as barely visible gray outline SDL_SetRenderDrawColor(renderer, 180, 180, 180, 20); // Very faint gray SDL_FRect rect = {px + 2, py + 2, tileSize - 4, tileSize - 4}; SDL_RenderFillRect(renderer, &rect); // Draw thin gray border SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30); SDL_FRect border = {px + 1, py + 1, tileSize - 2, tileSize - 2}; SDL_RenderRect(renderer, &border); } else { drawBlockTexture(renderer, blocksTex, px, py, tileSize, piece.type); } } } } } void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) { if (pieceType >= PIECE_COUNT) return; // Use the first rotation (index 0) for preview Game::Piece previewPiece; previewPiece.type = pieceType; previewPiece.rot = 0; previewPiece.x = 0; previewPiece.y = 0; // Center the piece in the preview area float offsetX = 0, offsetY = 0; if (pieceType == 0) { offsetX = tileSize * 0.5f; } // I-piece centering (assuming I = 0) else if (pieceType == 1) { offsetX = tileSize * 0.5f; } // O-piece centering (assuming O = 1) // Use semi-transparent alpha for preview blocks Uint8 previewAlpha = 180; if (blocksTex) { SDL_SetTextureAlphaMod(blocksTex, previewAlpha); } for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (Game::cellFilled(previewPiece, cx, cy)) { float px = x + offsetX + cx * tileSize; float py = y + offsetY + cy * tileSize; drawBlockTexture(renderer, blocksTex, px, py, tileSize, pieceType); } } } // Reset alpha if (blocksTex) { SDL_SetTextureAlphaMod(blocksTex, 255); } } // Draw the hold panel (extracted for readability). static void drawHoldPanel(SDL_Renderer* renderer, Game* game, FontAtlas* pixelFont, SDL_Texture* blocksTex, SDL_Texture* holdPanelTex, float scoreX, float statsW, float gridY, float finalBlockSize, float statsY, float statsH) { float holdBlockH = (finalBlockSize * 0.6f) * 4.0f; // Base panel height; enforce minimum but allow larger to fit texture float panelH = std::max(holdBlockH + 12.0f, 420.0f); // Increase height by ~20% of the hold block to give more vertical room float extraH = holdBlockH * 0.20f; panelH += extraH; const float holdGap = 18.0f; // Align X to the bottom score label (`scoreX`) plus an offset to the right float panelX = scoreX + 30.0f; // move ~30px right to align with score label float panelW = statsW + 32.0f; float panelY = gridY - panelH - holdGap; // Move panel a bit higher for spacing (about half the extra height) panelY -= extraH * 0.5f; float labelX = panelX + 40.0f; // shift HOLD label ~30px to the right float labelY = panelY + 8.0f; if (holdPanelTex) { int texW = 0, texH = 0; SDL_QueryTexture(holdPanelTex, nullptr, nullptr, &texW, &texH); if (texW > 0 && texH > 0) { // Fill panel width and compute destination height from texture aspect ratio float texAspect = float(texH) / float(texW); float dstW = panelW; float dstH = dstW * texAspect; // If texture height exceeds panel, expand panelH to fit texture comfortably if (dstH + 12.0f > panelH) { panelH = dstH + 12.0f; panelY = gridY - panelH - holdGap; labelY = panelY + 8.0f; } float dstX = panelX; float dstY = panelY + (panelH - dstH) * 0.5f; SDL_FRect panelDst{dstX, dstY, dstW, dstH}; SDL_SetTextureBlendMode(holdPanelTex, SDL_BLENDMODE_BLEND); SDL_SetTextureScaleMode(holdPanelTex, SDL_SCALEMODE_LINEAR); SDL_RenderTexture(renderer, holdPanelTex, nullptr, &panelDst); } else { // Fallback to filling panel area if texture metrics unavailable SDL_SetRenderDrawColor(renderer, 12, 18, 32, 220); SDL_FRect panelDst{panelX, panelY, panelW, panelH}; SDL_RenderFillRect(renderer, &panelDst); } } else { SDL_SetRenderDrawColor(renderer, 12, 18, 32, 220); SDL_FRect panelDst{panelX, panelY, panelW, panelH}; SDL_RenderFillRect(renderer, &panelDst); } pixelFont->draw(renderer, labelX, labelY, "HOLD", 1.0f, {255, 220, 0, 255}); if (game->held().type < PIECE_COUNT) { float previewW = finalBlockSize * 0.6f * 4.0f; float previewX = panelX + (panelW - previewW) * 0.5f; float previewY = panelY + (panelH - holdBlockH) * 0.5f; drawSmallPiece(renderer, blocksTex, static_cast(game->held().type), previewX, previewY, finalBlockSize * 0.6f); } } // Draw next piece panel (border/texture + preview) static void drawNextPanel(SDL_Renderer* renderer, FontAtlas* pixelFont, SDL_Texture* nextPanelTex, SDL_Texture* blocksTex, Game* game, float nextX, float nextY, float nextW, float nextH, float contentOffsetX, float contentOffsetY, float finalBlockSize) { if (nextPanelTex) { SDL_FRect dst{ nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH }; SDL_SetTextureBlendMode(nextPanelTex, SDL_BLENDMODE_BLEND); SDL_RenderTexture(renderer, nextPanelTex, nullptr, &dst); } else { // Draw bordered panel as before SDL_SetRenderDrawColor(renderer, 100, 120, 200, 255); SDL_FRect outer{ nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6 }; SDL_RenderFillRect(renderer, &outer); SDL_SetRenderDrawColor(renderer, 30, 35, 50, 255); SDL_FRect inner{ nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH }; SDL_RenderFillRect(renderer, &inner); } // Label and small preview pixelFont->draw(renderer, nextX + 10, nextY - 20, "NEXT", 1.0f, {255, 220, 0, 255}); if (game->next().type < PIECE_COUNT) { drawSmallPiece(renderer, blocksTex, static_cast(game->next().type), nextX + 10, nextY + 5, finalBlockSize * 0.6f); } } // Draw score panel (right side) static void drawScorePanel(SDL_Renderer* renderer, FontAtlas* pixelFont, Game* game, float scoreX, float gridY, float GRID_H, float finalBlockSize) { const float contentTopOffset = 0.0f; const float contentBottomOffset = 290.0f; const float contentPad = 36.0f; float scoreContentH = (contentBottomOffset - contentTopOffset) + contentPad; float baseY = gridY + (GRID_H - scoreContentH) * 0.5f; pixelFont->draw(renderer, scoreX, baseY + 0, "SCORE", 1.0f, {255, 220, 0, 255}); char scoreStr[32]; snprintf(scoreStr, sizeof(scoreStr), "%d", game->score()); pixelFont->draw(renderer, scoreX, baseY + 25, scoreStr, 0.9f, {255, 255, 255, 255}); pixelFont->draw(renderer, scoreX, baseY + 70, "LINES", 1.0f, {255, 220, 0, 255}); char linesStr[16]; snprintf(linesStr, sizeof(linesStr), "%03d", game->lines()); pixelFont->draw(renderer, scoreX, baseY + 95, linesStr, 0.9f, {255, 255, 255, 255}); pixelFont->draw(renderer, scoreX, baseY + 140, "LEVEL", 1.0f, {255, 220, 0, 255}); char levelStr[16]; snprintf(levelStr, sizeof(levelStr), "%02d", game->level()); pixelFont->draw(renderer, scoreX, baseY + 165, levelStr, 0.9f, {255, 255, 255, 255}); // Next level progress int startLv = game->startLevelBase(); int firstThreshold = (startLv + 1) * 10; int linesDone = game->lines(); int nextThreshold = 0; if (linesDone < firstThreshold) { nextThreshold = firstThreshold; } else { int blocksPast = linesDone - firstThreshold; nextThreshold = firstThreshold + ((blocksPast / 10) + 1) * 10; } int linesForNext = std::max(0, nextThreshold - linesDone); pixelFont->draw(renderer, scoreX, baseY + 200, "NEXT LVL", 1.0f, {255, 220, 0, 255}); char nextStr[32]; snprintf(nextStr, sizeof(nextStr), "%d LINES", linesForNext); pixelFont->draw(renderer, scoreX, baseY + 225, nextStr, 0.9f, {80, 255, 120, 255}); // Time display pixelFont->draw(renderer, scoreX, baseY + 265, "TIME", 1.0f, {255, 220, 0, 255}); int totalSecs = static_cast(game->elapsed()); int mins = totalSecs / 60; int secs = totalSecs % 60; char timeStr[16]; snprintf(timeStr, sizeof(timeStr), "%02d:%02d", mins, secs); pixelFont->draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255}); } void GameRenderer::renderPlayingState( SDL_Renderer* renderer, Game* game, FontAtlas* pixelFont, LineEffect* lineEffect, SDL_Texture* blocksTex, SDL_Texture* statisticsPanelTex, SDL_Texture* scorePanelTex, SDL_Texture* nextPanelTex, SDL_Texture* holdPanelTex, float logicalW, float logicalH, float logicalScale, float winW, float winH, bool showExitConfirmPopup, int exitPopupSelectedButton, bool suppressPauseVisuals ) { (void)exitPopupSelectedButton; if (!game || !pixelFont) return; // Calculate actual content area (centered within the window) float contentScale = logicalScale; float contentW = logicalW * contentScale; float contentH = logicalH * contentScale; float contentOffsetX = (winW - contentW) * 0.5f / contentScale; float contentOffsetY = (winH - contentH) * 0.5f / contentScale; // Helper lambda for drawing rectangles with content offset auto drawRectWithOffset = [&](float x, float y, float w, float h, SDL_Color c) { SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h}; SDL_RenderFillRect(renderer, &fr); }; // Responsive layout that scales with window size while maintaining margins const float MIN_MARGIN = 40.0f; const float TOP_MARGIN = 60.0f; const float PANEL_WIDTH = 180.0f; const float PANEL_SPACING = 30.0f; const float NEXT_PIECE_HEIGHT = 120.0f; const float BOTTOM_MARGIN = 60.0f; float yCursor = statsY + 44.0f; const float availableWidth = logicalW - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2); const float availableHeight = logicalH - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PIECE_HEIGHT; const float maxBlockSizeW = availableWidth / Game::COLS; // Draw hold panel via helper drawHoldPanel(renderer, game, pixelFont, blocksTex, holdPanelTex, scoreX, statsW, gridY, finalBlockSize, statsY, statsH); // Draw grid lines (solid so grid remains legible over background) SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255); for (int x = 1; x < Game::COLS; ++x) { float lineX = gridX + x * finalBlockSize; SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H); } for (int y = 1; y < Game::ROWS; ++y) { 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}); drawRectWithOffset(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255}); // Draw next piece panel drawNextPanel(renderer, pixelFont, nextPanelTex, blocksTex, game, nextX, nextY, nextW, nextH, contentOffsetX, contentOffsetY, finalBlockSize); // Draw the game board const auto &board = game->boardRef(); for (int y = 0; y < Game::ROWS; ++y) { for (int x = 0; x < Game::COLS; ++x) { int v = board[y * Game::COLS + x]; if (v > 0) { float bx = gridX + x * finalBlockSize; float by = gridY + y * finalBlockSize; drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1); } } } bool allowActivePieceRender = !game->isPaused() || suppressPauseVisuals; // Draw ghost piece (where current piece will land) if (allowActivePieceRender) { Game::Piece ghostPiece = game->current(); // Find landing position while (true) { Game::Piece testPiece = ghostPiece; testPiece.y++; bool collision = false; // Simple collision check for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (Game::cellFilled(testPiece, cx, cy)) { int gx = testPiece.x + cx; int gy = testPiece.y + cy; if (gy >= Game::ROWS || gx < 0 || gx >= Game::COLS || (gy >= 0 && board[gy * Game::COLS + gx] != 0)) { collision = true; break; } } } if (collision) break; } if (collision) break; ghostPiece = testPiece; } // Draw ghost piece drawPiece(renderer, blocksTex, ghostPiece, gridX, gridY, finalBlockSize, true); } // Draw the falling piece if (allowActivePieceRender) { drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false); } // Draw line clearing effects if (lineEffect && lineEffect->isActive()) { lineEffect->render(renderer, static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); } float yCursor = statsY + 44.0f; // Draw next piece preview pixelFont->draw(renderer, nextX + 10, nextY - 20, "NEXT", 1.0f, {255, 220, 0, 255}); if (game->next().type < PIECE_COUNT) { // Nudge preview slightly upward to remove thin bottom artifact drawSmallPiece(renderer, blocksTex, static_cast(game->next().type), nextX + 10, nextY + 5, finalBlockSize * 0.6f); } // Draw block statistics (left panel) pixelFont->draw(renderer, statsX + 10, statsY + 10, "BLOCKS", 1.0f, {255, 220, 0, 255}); const auto& blockCounts = game->getBlockCounts(); float previewY = rowTop - 4.0f; for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i]; const float rowPadding = 18.0f; const float rowWidth = statsW - rowPadding * 2.0f; const float rowSpacing = 12.0f; float yCursor = statsY + 44.0f; for (int i = 0; i < PIECE_COUNT; ++i) { float rowTop = yCursor; float previewSize = finalBlockSize * 0.52f; float previewX = statsX + rowPadding; float previewY = rowTop - 14.0f; // Determine actual piece height so bars never overlap blocks Game::Piece previewPiece{}; previewPiece.type = static_cast(i); previewPiece.rot = 0; previewPiece.x = 0; previewPiece.y = 0; int maxCy = -1; for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (Game::cellFilled(previewPiece, cx, cy)) { maxCy = std::max(maxCy, cy); } } } float pieceHeight = (maxCy >= 0 ? maxCy + 1.0f : 1.0f) * previewSize; int count = blockCounts[i]; char countStr[16]; snprintf(countStr, sizeof(countStr), "%d", count); int countW = 0, countH = 0; pixelFont->measure(countStr, 1.0f, countW, countH); // Horizontal shift to push the counts/percent a bit more to the right const float statsNumbersShift = 20.0f; // Small left shift for progress bar so the track aligns better with the design const float statsBarShift = -10.0f; float countX = previewX + rowWidth - static_cast(countW) + statsNumbersShift; float countY = previewY + 9.0f; int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(count) / double(totalBlocks))) : 0; char percStr[16]; snprintf(percStr, sizeof(percStr), "%d%%", perc); int percW = 0, percH = 0; pixelFont->measure(percStr, 0.8f, percW, percH); float barX = previewX + statsBarShift; float barY = previewY + pieceHeight + 12.0f; float barH = 6.0f; float barW = rowWidth; float percY = barY + barH + 8.0f; float fillW = barW * (perc / 100.0f); fillW = std::clamp(fillW, 0.0f, barW); float cardTop = rowTop - 14.0f; float rowBottom = percY + 16.0f; SDL_FRect rowBg{ previewX - 12.0f, cardTop, rowWidth + 24.0f, rowBottom - cardTop }; SDL_SetRenderDrawColor(renderer, 18, 26, 40, 200); SDL_RenderFillRect(renderer, &rowBg); SDL_SetRenderDrawColor(renderer, 70, 100, 150, 210); SDL_RenderRect(renderer, &rowBg); drawSmallPiece(renderer, blocksTex, static_cast(i), previewX, previewY, previewSize); pixelFont->draw(renderer, countX, countY, countStr, 1.0f, {245, 245, 255, 255}); // Draw percent right-aligned near the same right edge as the count float percX = previewX + rowWidth - static_cast(percW) + statsNumbersShift; pixelFont->draw(renderer, percX, percY, percStr, 0.8f, {215, 225, 240, 255}); SDL_SetRenderDrawColor(renderer, 110, 120, 140, 200); SDL_FRect track{barX, barY, barW, barH}; SDL_RenderFillRect(renderer, &track); SDL_Color pc = COLORS[i + 1]; SDL_SetRenderDrawColor(renderer, pc.r, pc.g, pc.b, 255); SDL_FRect fill{barX, barY, fillW, barH}; SDL_RenderFillRect(renderer, &fill); yCursor = rowBottom + rowSpacing; } // Draw score panel drawScorePanel(renderer, pixelFont, game, scoreX, gridY, GRID_H, finalBlockSize); // Gravity HUD char gms[64]; double gms_val = game->getGravityMs(); double gfps = gms_val > 0.0 ? (1000.0 / gms_val) : 0.0; snprintf(gms, sizeof(gms), "GRAV: %.0f ms (%.2f fps)", gms_val, gfps); pixelFont->draw(renderer, logicalW - 260, 10, gms, 0.9f, {200, 200, 220, 255}); // Hold panel (always visible): draw background & label; preview shown only when a piece is held. { float holdBlockH = (finalBlockSize * 0.6f) * 4.0f; // Base panel height; enforce minimum but allow larger to fit texture float panelH = std::max(holdBlockH + 12.0f, 420.0f); // Increase height by ~20% of the hold block to give more vertical room float extraH = holdBlockH * 0.50f; panelH += extraH; const float holdGap = 18.0f; // Align X to the bottom score label (`scoreX`) plus an offset to the right float panelX = scoreX + 30.0f; // move ~30px right to align with score label float panelW = statsW + 32.0f; float panelY = gridY - panelH - holdGap; // Move panel a bit higher for spacing (about half the extra height) panelY -= extraH * 0.5f; float labelX = panelX + 40.0f; // shift HOLD label ~30px to the right float labelY = panelY + 8.0f; if (holdPanelTex) { int texW = 0, texH = 0; SDL_QueryTexture(holdPanelTex, nullptr, nullptr, &texW, &texH); if (texW > 0 && texH > 0) { // If the texture is taller than the current panel, expand panelH float texAspect = float(texH) / float(texW); float desiredTexH = panelW * texAspect; if (desiredTexH + 12.0f > panelH) { panelH = desiredTexH + 12.0f; // Recompute vertical placement after growing panelH panelY = gridY - panelH - holdGap; labelY = panelY + 8.0f; } // Fill panel width and compute destination height from texture aspect ratio float texAspect = float(texH) / float(texW); float dstW = panelW; float dstH = dstW * texAspect * 1.2f; // If texture height exceeds panel, expand panelH to fit texture comfortably if (dstH + 12.0f > panelH) { panelH = dstH + 12.0f; panelY = gridY - panelH - holdGap; labelY = panelY + 8.0f; } float dstX = panelX; float dstY = panelY + (panelH - dstH) * 0.5f; SDL_FRect panelDst{dstX, dstY, dstW, dstH}; SDL_SetTextureBlendMode(holdPanelTex, SDL_BLENDMODE_BLEND); SDL_SetTextureScaleMode(holdPanelTex, SDL_SCALEMODE_LINEAR); SDL_RenderTexture(renderer, holdPanelTex, nullptr, &panelDst); } else { // Fallback to filling panel area if texture metrics unavailable SDL_SetRenderDrawColor(renderer, 12, 18, 32, 220); SDL_FRect panelDst{panelX, panelY, panelW, panelH}; SDL_RenderFillRect(renderer, &panelDst); } } else { SDL_SetRenderDrawColor(renderer, 12, 18, 32, 220); SDL_FRect panelDst{panelX, panelY, panelW, panelH}; SDL_RenderFillRect(renderer, &panelDst); } pixelFont->draw(renderer, labelX, labelY, "HOLD", 1.0f, {255, 220, 0, 255}); if (game->held().type < PIECE_COUNT) { float previewW = finalBlockSize * 0.6f * 4.0f; float previewX = panelX + (panelW - previewW) * 0.5f; float previewY = panelY + (panelH - holdBlockH) * 0.5f; drawSmallPiece(renderer, blocksTex, static_cast(game->held().type), previewX, previewY, finalBlockSize * 0.6f); } } // Pause overlay (suppressed when requested, e.g., countdown) if (!suppressPauseVisuals && game->isPaused() && !showExitConfirmPopup) { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); for (int i = -4; i <= 4; ++i) { float spread = static_cast(std::abs(i)); Uint8 alpha = Uint8(std::max(8.f, 32.f - spread * 4.f)); SDL_SetRenderDrawColor(renderer, 24, 32, 48, alpha); SDL_FRect blurRect{ gridX - spread * 2.0f, gridY - spread * 1.5f, GRID_W + spread * 4.0f, GRID_H + spread * 3.0f }; SDL_RenderFillRect(renderer, &blurRect); } SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); SDL_FRect pauseOverlay{0, 0, logicalW, logicalH}; SDL_RenderFillRect(renderer, &pauseOverlay); pixelFont->draw(renderer, logicalW * 0.5f - 80, logicalH * 0.5f - 20, "PAUSED", 2.0f, {255, 255, 255, 255}); pixelFont->draw(renderer, logicalW * 0.5f - 120, logicalH * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255}); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); } if (showExitConfirmPopup) { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200); SDL_FRect fullWin{0.f, 0.f, winW, winH}; SDL_RenderFillRect(renderer, &fullWin); const float panelW = 640.0f; const float panelH = 320.0f; SDL_FRect panel{ (logicalW - panelW) * 0.5f + contentOffsetX, (logicalH - panelH) * 0.5f + contentOffsetY, panelW, panelH }; SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h}; SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140); SDL_RenderFillRect(renderer, &shadow); for (int i = 0; i < 5; ++i) { SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)}; SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(44 - i * 7)); SDL_RenderRect(renderer, &glow); } SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255); SDL_RenderFillRect(renderer, &panel); SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255); SDL_RenderRect(renderer, &panel); SDL_FRect inner{panel.x + 24.0f, panel.y + 98.0f, panel.w - 48.0f, panel.h - 146.0f}; SDL_SetRenderDrawColor(renderer, 16, 24, 40, 235); SDL_RenderFillRect(renderer, &inner); SDL_SetRenderDrawColor(renderer, 40, 80, 140, 235); SDL_RenderRect(renderer, &inner); const std::string title = "EXIT GAME?"; int titleW = 0, titleH = 0; const float titleScale = 1.8f; pixelFont->measure(title, titleScale, titleW, titleH); pixelFont->draw(renderer, panel.x + (panel.w - titleW) * 0.5f, panel.y + 30.0f, title, titleScale, {255, 230, 140, 255}); std::array lines = { "Are you sure you want to quit?", "Current progress will be lost." }; float lineY = inner.y + 22.0f; const float lineScale = 1.05f; for (const auto& line : lines) { int lineW = 0, lineH = 0; pixelFont->measure(line, lineScale, lineW, lineH); float textX = panel.x + (panel.w - lineW) * 0.5f; pixelFont->draw(renderer, textX, lineY, line, lineScale, SDL_Color{210, 220, 240, 255}); lineY += lineH + 10.0f; } const float horizontalPad = 28.0f; const float buttonGap = 32.0f; const float buttonH = 66.0f; float buttonW = (inner.w - horizontalPad * 2.0f - buttonGap) * 0.5f; float buttonY = inner.y + inner.h - buttonH - 24.0f; auto drawButton = [&](float x, const char* label, SDL_Color base) { SDL_FRect btn{x, buttonY, buttonW, buttonH}; SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120); SDL_FRect btnShadow{btn.x + 4.0f, btn.y + 6.0f, btn.w, btn.h}; SDL_RenderFillRect(renderer, &btnShadow); SDL_SetRenderDrawColor(renderer, base.r, base.g, base.b, base.a); SDL_RenderFillRect(renderer, &btn); SDL_SetRenderDrawColor(renderer, 90, 130, 200, 255); SDL_RenderRect(renderer, &btn); int textW = 0, textH = 0; const float labelScale = 1.4f; pixelFont->measure(label, labelScale, textW, textH); float textX = btn.x + (btn.w - textW) * 0.5f; float textY = btn.y + (btn.h - textH) * 0.5f; pixelFont->draw(renderer, textX, textY, label, labelScale, SDL_Color{255, 255, 255, 255}); }; float yesX = inner.x + horizontalPad; float noX = yesX + buttonW + buttonGap; drawButton(yesX, "YES", SDL_Color{185, 70, 70, 255}); drawButton(noX, "NO", SDL_Color{60, 95, 150, 255}); } }