#include "GameRenderer.h" #include "../../gameplay/core/Game.h" #include "../ui/Font.h" #include "../../gameplay/effects/LineEffect.h" #include #include #include #include #include #include #include #include "../../core/Settings.h" namespace { struct ImpactSpark { float x = 0.0f; float y = 0.0f; float vx = 0.0f; float vy = 0.0f; float lifeMs = 0.0f; float maxLifeMs = 0.0f; float size = 0.0f; SDL_Color color{255, 255, 255, 255}; }; struct ActivePieceSmoothState { uint64_t sequence = 0; float visualX = 0.0f; bool initialized = false; }; ActivePieceSmoothState s_activePieceSmooth; } // 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, float pixelOffsetX, float pixelOffsetY) { 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 + pixelOffsetX; float py = oy + (piece.y + cy) * tileSize + pixelOffsetY; 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); } } void GameRenderer::renderPlayingState( SDL_Renderer* renderer, Game* game, FontAtlas* pixelFont, LineEffect* lineEffect, SDL_Texture* blocksTex, float logicalW, float logicalH, float logicalScale, float winW, float winH ) { if (!game || !pixelFont) return; static std::vector s_impactSparks; static uint32_t s_lastImpactFxId = 0; static Uint32 s_lastImpactTick = SDL_GetTicks(); static std::mt19937 s_impactRng{ std::random_device{}() }; Uint32 nowTicks = SDL_GetTicks(); float sparkDeltaMs = static_cast(nowTicks - s_lastImpactTick); s_lastImpactTick = nowTicks; if (sparkDeltaMs < 0.0f || sparkDeltaMs > 100.0f) { sparkDeltaMs = 16.0f; } // 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; // Calculate layout dimensions 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; const float maxBlockSizeH = availableHeight / Game::ROWS; const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH); const float finalBlockSize = std::max(20.0f, std::min(BLOCK_SIZE, 40.0f)); const float GRID_W = Game::COLS * finalBlockSize; const float GRID_H = Game::ROWS * finalBlockSize; // Calculate positions const float totalContentHeight = NEXT_PIECE_HEIGHT + GRID_H; const float availableVerticalSpace = logicalH - TOP_MARGIN - BOTTOM_MARGIN; const float verticalCenterOffset = (availableVerticalSpace - totalContentHeight) * 0.5f; const float contentStartY = TOP_MARGIN + verticalCenterOffset; const float totalLayoutWidth = PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + PANEL_WIDTH; const float layoutStartX = (logicalW - totalLayoutWidth) * 0.5f; const float statsX = layoutStartX + contentOffsetX; const float gridX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + contentOffsetX; const float scoreX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + contentOffsetX; const float gridY = contentStartY + NEXT_PIECE_HEIGHT + contentOffsetY; const float statsY = gridY; const float statsW = PANEL_WIDTH; const float statsH = GRID_H; // Next piece preview position const float nextW = finalBlockSize * 4 + 20; const float nextH = finalBlockSize * 2 + 20; const float nextX = gridX + (GRID_W - nextW) * 0.5f; const float nextY = contentStartY + contentOffsetY; // Handle line clearing effects if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) { auto completedLines = game->getCompletedLines(); lineEffect->startLineClear(completedLines, static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); } // Draw game grid border drawRectWithOffset(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255}); drawRectWithOffset(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255}); drawRectWithOffset(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255}); // Draw panel backgrounds SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160); SDL_FRect lbg{statsX - 16, gridY - 10, statsW + 32, GRID_H + 20}; SDL_RenderFillRect(renderer, &lbg); SDL_FRect rbg{scoreX - 16, gridY - 16, statsW + 32, GRID_H + 32}; SDL_RenderFillRect(renderer, &rbg); // Draw grid lines 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); } // 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 preview panel border drawRectWithOffset(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255}); drawRectWithOffset(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255}); // Precompute row drop offsets (line collapse effect) std::array rowDropOffsets{}; for (int y = 0; y < Game::ROWS; ++y) { rowDropOffsets[y] = (lineEffect ? lineEffect->getRowDropOffset(y) : 0.0f); } // Draw the game board const auto &board = game->boardRef(); float impactStrength = 0.0f; float impactEased = 0.0f; std::array impactMask{}; std::array impactWeight{}; if (game->hasHardDropShake()) { impactStrength = static_cast(game->hardDropShakeStrength()); impactStrength = std::clamp(impactStrength, 0.0f, 1.0f); impactEased = impactStrength * impactStrength; const auto& impactCells = game->getHardDropCells(); for (const auto& cell : impactCells) { if (cell.x < 0 || cell.x >= Game::COLS || cell.y < 0 || cell.y >= Game::ROWS) { continue; } int idx = cell.y * Game::COLS + cell.x; impactMask[idx] = 1; impactWeight[idx] = 1.0f; int depth = 0; for (int ny = cell.y + 1; ny < Game::ROWS && depth < 4; ++ny) { if (board[ny * Game::COLS + cell.x] == 0) { break; } ++depth; int nidx = ny * Game::COLS + cell.x; impactMask[nidx] = 1; float weight = std::max(0.15f, 1.0f - depth * 0.35f); impactWeight[nidx] = std::max(impactWeight[nidx], weight); } } } bool shouldSpawnCrackles = game->hasHardDropShake() && !game->getHardDropCells().empty() && game->getHardDropFxId() != s_lastImpactFxId; if (shouldSpawnCrackles) { s_lastImpactFxId = game->getHardDropFxId(); std::uniform_real_distribution jitter(-finalBlockSize * 0.2f, finalBlockSize * 0.2f); std::uniform_real_distribution velX(-0.04f, 0.04f); std::uniform_real_distribution velY(0.035f, 0.07f); std::uniform_real_distribution lifespan(210.0f, 320.0f); std::uniform_real_distribution sizeDist(finalBlockSize * 0.08f, finalBlockSize * 0.14f); const auto& impactCells = game->getHardDropCells(); for (const auto& cell : impactCells) { if (cell.x < 0 || cell.x >= Game::COLS || cell.y < 0 || cell.y >= Game::ROWS) { continue; } int idx = cell.y * Game::COLS + cell.x; int v = (cell.y >= 0) ? board[idx] : 0; SDL_Color baseColor = (v > 0 && v < PIECE_COUNT + 1) ? COLORS[v] : SDL_Color{255, 220, 180, 255}; float cellX = gridX + (cell.x + 0.5f) * finalBlockSize; float cellY = gridY + (cell.y + 0.85f) * finalBlockSize + rowDropOffsets[cell.y]; for (int i = 0; i < 4; ++i) { ImpactSpark spark; spark.x = cellX + jitter(s_impactRng); spark.y = cellY + jitter(s_impactRng) * 0.25f; spark.vx = velX(s_impactRng); spark.vy = velY(s_impactRng); spark.lifeMs = lifespan(s_impactRng); spark.maxLifeMs = spark.lifeMs; spark.size = sizeDist(s_impactRng); spark.color = SDL_Color{ static_cast(std::min(255, baseColor.r + 30)), static_cast(std::min(255, baseColor.g + 30)), static_cast(std::min(255, baseColor.b + 30)), 255 }; s_impactSparks.push_back(spark); } } } for (int y = 0; y < Game::ROWS; ++y) { float dropOffset = rowDropOffsets[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 + dropOffset; const int cellIdx = y * Game::COLS + x; float weight = impactWeight[cellIdx]; if (impactStrength > 0.0f && weight > 0.0f && impactMask[cellIdx]) { float cellSeed = static_cast((x * 37 + y * 61) % 113); float t = static_cast(nowTicks % 10000) * 0.018f + cellSeed; float amplitude = 6.0f * impactEased * weight; float freq = 2.0f + weight * 1.3f; bx += amplitude * std::sin(t * freq); by += amplitude * 0.75f * std::cos(t * (freq + 1.1f)); } drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1); } } } if (!s_impactSparks.empty()) { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); auto it = s_impactSparks.begin(); while (it != s_impactSparks.end()) { ImpactSpark& spark = *it; spark.vy += 0.00045f * sparkDeltaMs; spark.x += spark.vx * sparkDeltaMs; spark.y += spark.vy * sparkDeltaMs; spark.lifeMs -= sparkDeltaMs; if (spark.lifeMs <= 0.0f) { it = s_impactSparks.erase(it); continue; } float lifeRatio = spark.lifeMs / spark.maxLifeMs; Uint8 alpha = static_cast(std::clamp(lifeRatio, 0.0f, 1.0f) * 160.0f); SDL_SetRenderDrawColor(renderer, spark.color.r, spark.color.g, spark.color.b, alpha); SDL_FRect sparkRect{ spark.x - spark.size * 0.5f, spark.y - spark.size * 0.5f, spark.size, spark.size * 1.4f }; SDL_RenderFillRect(renderer, &sparkRect); ++it; } } bool allowActivePieceRender = true; const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled(); float activePiecePixelOffsetX = 0.0f; if (allowActivePieceRender) { if (smoothScrollEnabled && !game->isPaused()) { const uint64_t pieceSeq = game->getCurrentPieceSequence(); if (!s_activePieceSmooth.initialized || s_activePieceSmooth.sequence != pieceSeq) { s_activePieceSmooth.sequence = pieceSeq; s_activePieceSmooth.visualX = static_cast(game->current().x); s_activePieceSmooth.initialized = true; } const float targetX = static_cast(game->current().x); constexpr float HORIZONTAL_SMOOTH_MS = 55.0f; const float lerpFactor = std::clamp(sparkDeltaMs / HORIZONTAL_SMOOTH_MS, 0.0f, 1.0f); s_activePieceSmooth.visualX = std::lerp(s_activePieceSmooth.visualX, targetX, lerpFactor); activePiecePixelOffsetX = (s_activePieceSmooth.visualX - targetX) * finalBlockSize; } else { s_activePieceSmooth.sequence = game->getCurrentPieceSequence(); s_activePieceSmooth.visualX = static_cast(game->current().x); s_activePieceSmooth.initialized = true; } } auto computeFallOffset = [&]() -> float { if (game->isPaused()) { return 0.0f; } double gravityMs = game->getGravityMs(); if (gravityMs <= 0.0) { return 0.0f; } double effectiveMs = game->isSoftDropping() ? std::max(5.0, gravityMs / 5.0) : gravityMs; double accumulator = std::clamp(game->getFallAccumulator(), 0.0, effectiveMs); if (effectiveMs <= 0.0) { return 0.0f; } float progress = static_cast(accumulator / effectiveMs); progress = std::clamp(progress, 0.0f, 1.0f); return progress * finalBlockSize; }; float activePiecePixelOffsetY = (!game->isPaused() && smoothScrollEnabled) ? computeFallOffset() : 0.0f; if (activePiecePixelOffsetY > 0.0f) { const auto& boardRef = game->boardRef(); const Game::Piece& piece = game->current(); float maxAllowed = finalBlockSize; for (int cy = 0; cy < 4; ++cy) { for (int cx = 0; cx < 4; ++cx) { if (!Game::cellFilled(piece, cx, cy)) { continue; } int gx = piece.x + cx; int gy = piece.y + cy; if (gx < 0 || gx >= Game::COLS) { continue; } int testY = gy + 1; int emptyRows = 0; if (testY < 0) { emptyRows -= testY; // number of rows until we reach row 0 testY = 0; } while (testY >= 0 && testY < Game::ROWS) { if (boardRef[testY * Game::COLS + gx] != 0) { break; } ++emptyRows; ++testY; } float cellLimit = (emptyRows > 0) ? finalBlockSize : 0.0f; maxAllowed = std::min(maxAllowed, cellLimit); } } activePiecePixelOffsetY = std::min(activePiecePixelOffsetY, maxAllowed); } // 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, activePiecePixelOffsetX, activePiecePixelOffsetY); } // Draw line clearing effects if (lineEffect && lineEffect->isActive()) { lineEffect->render(renderer, blocksTex, static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); } // Draw next piece 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 + 10, 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(); int totalBlocks = 0; 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 preview height to keep bars below the blocks Game::Piece previewPiece{}; previewPiece.type = static_cast(i); 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); float countX = previewX + rowWidth - static_cast(countW); 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); float barX = previewX; float barY = previewY + pieceHeight + 12.0f; float barH = 6.0f; float barW = rowWidth; float percY = barY + barH + 8.0f; float rowBottom = percY + 16.0f; SDL_FRect rowBg{ previewX - 12.0f, rowTop - 14.0f, rowWidth + 24.0f, rowBottom - (rowTop - 14.0f) }; 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}); pixelFont->draw(renderer, previewX, 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); float fillW = barW * (perc / 100.0f); fillW = std::clamp(fillW, 0.0f, barW); SDL_FRect fill{barX, barY, fillW, barH}; SDL_RenderFillRect(renderer, &fill); yCursor = rowBottom + rowSpacing; } // Draw score panel (right side) 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}); // Debug: Gravity timing info if (Settings::instance().isDebugEnabled()) { pixelFont->draw(renderer, scoreX, baseY + 330, "GRAVITY", 0.8f, {150, 150, 150, 255}); double gravityMs = game->getGravityMs(); double fallAcc = game->getFallAccumulator(); // Calculate effective gravity (accounting for soft drop) bool isSoftDrop = game->isSoftDropping(); double effectiveGravityMs = isSoftDrop ? (gravityMs / 2.0) : gravityMs; double timeUntilDrop = std::max(0.0, effectiveGravityMs - fallAcc); char gravityStr[32]; snprintf(gravityStr, sizeof(gravityStr), "%.0f ms%s", gravityMs, isSoftDrop ? " (SD)" : ""); pixelFont->draw(renderer, scoreX, baseY + 350, gravityStr, 0.7f, {180, 180, 180, 255}); char dropStr[32]; snprintf(dropStr, sizeof(dropStr), "Drop: %.0f", timeUntilDrop); SDL_Color dropColor = isSoftDrop ? SDL_Color{255, 200, 100, 255} : SDL_Color{100, 255, 100, 255}; pixelFont->draw(renderer, scoreX, baseY + 370, dropStr, 0.7f, dropColor); // Gravity HUD (Top) 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 piece (if implemented) if (game->held().type < PIECE_COUNT) { pixelFont->draw(renderer, statsX + 10, statsY + statsH - 80, "HOLD", 1.0f, {255, 220, 0, 255}); drawSmallPiece(renderer, blocksTex, static_cast(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); } // Pause overlay logic moved to renderPauseOverlay // Exit popup logic moved to renderExitPopup } void GameRenderer::renderExitPopup( SDL_Renderer* renderer, FontAtlas* pixelFont, float winW, float winH, float logicalScale, int selectedButton ) { // Calculate content offsets (same as in renderPlayingState for consistency) // We need to re-calculate them or pass them in? // The popup uses logical coordinates centered on screen. // Let's use the same logic as renderPauseOverlay (window coordinates) to be safe and consistent? // The original code used logical coordinates + contentOffset. // Let's stick to the original look but render it in window coordinates to ensure it covers everything properly. SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); // Switch to window coordinates SDL_Rect oldViewport; SDL_GetRenderViewport(renderer, &oldViewport); float oldScaleX, oldScaleY; SDL_GetRenderScale(renderer, &oldScaleX, &oldScaleY); SDL_SetRenderViewport(renderer, nullptr); SDL_SetRenderScale(renderer, 1.0f, 1.0f); // Full screen dim SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200); SDL_FRect fullWin{0.f, 0.f, winW, winH}; SDL_RenderFillRect(renderer, &fullWin); // Calculate panel position (centered in window) // Original was logicalW based, let's map it to window size. // Logical 640x320 scaled up. float panelW = 640.0f * logicalScale; float panelH = 320.0f * logicalScale; float panelX = (winW - panelW) * 0.5f; float panelY = (winH - panelH) * 0.5f; SDL_FRect panel{panelX, panelY, panelW, panelH}; SDL_FRect shadow{panel.x + 6.0f * logicalScale, panel.y + 10.0f * logicalScale, panel.w, panel.h}; SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140); SDL_RenderFillRect(renderer, &shadow); for (int i = 0; i < 5; ++i) { float off = float(i * 2) * logicalScale; float exp = float(i * 4) * logicalScale; SDL_FRect glow{panel.x - off, panel.y - off, panel.w + exp, panel.h + exp}; 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 * logicalScale, panel.y + 98.0f * logicalScale, panel.w - 48.0f * logicalScale, panel.h - 146.0f * logicalScale}; 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 * logicalScale; pixelFont->measure(title, titleScale, titleW, titleH); pixelFont->draw(renderer, panel.x + (panel.w - titleW) * 0.5f, panel.y + 30.0f * logicalScale, 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 * logicalScale; const float lineScale = 1.05f * logicalScale; 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 * logicalScale; } const float horizontalPad = 28.0f * logicalScale; const float buttonGap = 32.0f * logicalScale; const float buttonH = 66.0f * logicalScale; float buttonW = (inner.w - horizontalPad * 2.0f - buttonGap) * 0.5f; float buttonY = inner.y + inner.h - buttonH - 24.0f * logicalScale; auto drawButton = [&](int idx, float x, const char* label) { bool selected = (selectedButton == idx); SDL_Color base = (idx == 0) ? SDL_Color{185, 70, 70, 255} : SDL_Color{60, 95, 150, 255}; SDL_Color body = selected ? SDL_Color{Uint8(std::min(255, base.r + 35)), Uint8(std::min(255, base.g + 35)), Uint8(std::min(255, base.b + 35)), 255} : base; SDL_Color border = selected ? SDL_Color{255, 220, 120, 255} : SDL_Color{80, 110, 160, 255}; SDL_FRect btn{x, buttonY, buttonW, buttonH}; SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120); SDL_FRect btnShadow{btn.x + 4.0f * logicalScale, btn.y + 6.0f * logicalScale, btn.w, btn.h}; SDL_RenderFillRect(renderer, &btnShadow); SDL_SetRenderDrawColor(renderer, body.r, body.g, body.b, body.a); SDL_RenderFillRect(renderer, &btn); SDL_SetRenderDrawColor(renderer, border.r, border.g, border.b, border.a); SDL_RenderRect(renderer, &btn); int textW = 0, textH = 0; const float labelScale = 1.4f * logicalScale; pixelFont->measure(label, labelScale, textW, textH); float textX = btn.x + (btn.w - textW) * 0.5f; float textY = btn.y + (btn.h - textH) * 0.5f; SDL_Color textColor = selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{230, 235, 250, 255}; pixelFont->draw(renderer, textX, textY, label, labelScale, textColor); }; float yesX = inner.x + horizontalPad; float noX = yesX + buttonW + buttonGap; drawButton(0, yesX, "YES"); drawButton(1, noX, "NO"); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); // Restore previous render state SDL_SetRenderViewport(renderer, &oldViewport); SDL_SetRenderScale(renderer, oldScaleX, oldScaleY); } void GameRenderer::renderPauseOverlay( SDL_Renderer* renderer, FontAtlas* pixelFont, float winW, float winH, float logicalScale ) { SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); // Switch to window coordinates for the full-screen overlay and text SDL_Rect oldViewport; SDL_GetRenderViewport(renderer, &oldViewport); float oldScaleX, oldScaleY; SDL_GetRenderScale(renderer, &oldScaleX, &oldScaleY); SDL_SetRenderViewport(renderer, nullptr); SDL_SetRenderScale(renderer, 1.0f, 1.0f); // Draw full screen overlay (darken) SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); SDL_FRect pauseOverlay{0, 0, winW, winH}; SDL_RenderFillRect(renderer, &pauseOverlay); // Draw centered text const char* pausedText = "PAUSED"; float pausedScale = 2.0f * logicalScale; int pW = 0, pH = 0; pixelFont->measure(pausedText, pausedScale, pW, pH); pixelFont->draw(renderer, (winW - pW) * 0.5f, (winH - pH) * 0.5f - (20 * logicalScale), pausedText, pausedScale, {255, 255, 255, 255}); const char* resumeText = "Press P to resume"; float resumeScale = 0.8f * logicalScale; int rW = 0, rH = 0; pixelFont->measure(resumeText, resumeScale, rW, rH); pixelFont->draw(renderer, (winW - rW) * 0.5f, (winH - pH) * 0.5f + (40 * logicalScale), resumeText, resumeScale, {200, 200, 220, 255}); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); // Restore previous render state SDL_SetRenderViewport(renderer, &oldViewport); SDL_SetRenderScale(renderer, oldScaleX, oldScaleY); }