#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); } } 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, 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; const float maxBlockSizeH = availableHeight / Game::ROWS; float previewY = rowTop - 4.0f; 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; float barY = previewY + previewSize + 10.0f; 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; float rowBottom = percY + 14.0f; SDL_FRect rowBg{ previewX - 10.0f, rowTop - 8.0f, rowWidth + 20.0f, rowBottom - rowTop }; 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); } 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 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}); // 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) { 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(); 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); 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 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}); 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); 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}); // 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 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 (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}); } }