diff --git a/assets/images/panel_score.png b/assets/images/panel_score.png index 5042621..c0304cc 100644 Binary files a/assets/images/panel_score.png and b/assets/images/panel_score.png differ diff --git a/assets/images/statistics_panel.png b/assets/images/statistics_panel.png new file mode 100644 index 0000000..116c3de Binary files /dev/null and b/assets/images/statistics_panel.png differ diff --git a/src/core/application/ApplicationManager.cpp b/src/core/application/ApplicationManager.cpp index 2f58484..ace8ebc 100644 --- a/src/core/application/ApplicationManager.cpp +++ b/src/core/application/ApplicationManager.cpp @@ -1131,6 +1131,7 @@ void ApplicationManager::setupStateHandlers() { m_stateContext.pixelFont, m_stateContext.lineEffect, m_stateContext.blocksTex, + m_stateContext.statisticsPanelTex, m_stateContext.scorePanelTex, LOGICAL_W, LOGICAL_H, diff --git a/src/gameplay/core/Game.cpp b/src/gameplay/core/Game.cpp index de33c32..1381a63 100644 --- a/src/gameplay/core/Game.cpp +++ b/src/gameplay/core/Game.cpp @@ -55,6 +55,9 @@ void Game::reset(int startLevel_) { std::fill(blockCounts.begin(), blockCounts.end(), 0); bag.clear(); _score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_; + _tetrisesMade = 0; + _currentCombo = 0; + _maxCombo = 0; // Initialize gravity using NES timing table (ms per cell by level) gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier); fallAcc = 0; gameOver=false; paused=false; @@ -218,6 +221,15 @@ void Game::lockPiece() { // Update total lines _lines += cleared; + // Update combo counters: consecutive clears increase combo; reset when no clear + _currentCombo += 1; + if (_currentCombo > _maxCombo) _maxCombo = _currentCombo; + + // Track tetrises made + if (cleared == 4) { + _tetrisesMade += 1; + } + // JS level progression (NES-like) using starting level rules // Both startLevel and _level are 0-based now. int targetLevel = startLevel; @@ -242,7 +254,10 @@ void Game::lockPiece() { soundCallback(cleared); } } - + else { + // No clear -> reset combo + _currentCombo = 0; + } if (!gameOver) spawn(); } diff --git a/src/gameplay/core/Game.h b/src/gameplay/core/Game.h index 63774bc..fadc14b 100644 --- a/src/gameplay/core/Game.h +++ b/src/gameplay/core/Game.h @@ -81,6 +81,9 @@ public: const std::vector& getHardDropCells() const { return hardDropCells; } uint32_t getHardDropFxId() const { return hardDropFxId; } uint64_t getCurrentPieceSequence() const { return pieceSequence; } + // Additional stats + int tetrisesMade() const { return _tetrisesMade; } + int maxCombo() const { return _maxCombo; } private: std::array board{}; // 0 empty else color index @@ -94,6 +97,9 @@ private: int _score{0}; int _lines{0}; int _level{1}; + int _tetrisesMade{0}; + int _currentCombo{0}; + int _maxCombo{0}; double gravityMs{800.0}; double fallAcc{0.0}; Uint64 _startTime{0}; // Performance counter at game start diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 35b83a8..ed126aa 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -61,6 +61,10 @@ struct TransportEffectState { float targetX = 0.0f; float targetY = 0.0f; float tileSize = 24.0f; + // Next preview that should fade in after the transfer completes + Game::Piece nextPiece; + float nextPreviewX = 0.0f; + float nextPreviewY = 0.0f; }; static TransportEffectState s_transport; @@ -82,50 +86,177 @@ void GameRenderer::startTransportEffect(const Game::Piece& piece, float startX, s_transport.tileSize = tileSize; } +void GameRenderer::startTransportEffectForGame(Game* game, SDL_Texture* blocksTex, float logicalW, float logicalH, float logicalScale, float winW, float winH, float durationSeconds) { + if (!game) return; + + // Recompute layout exactly like renderPlayingState so coordinates match + 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 contentScale = logicalScale; + float contentW = logicalW * contentScale; + float contentH = logicalH * contentScale; + float contentOffsetX = (winW - contentW) * 0.5f / contentScale; + float contentOffsetY = (winH - contentH) * 0.5f / contentScale; + + 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; + 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 gridX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + contentOffsetX; + const float gridY = contentStartY + NEXT_PIECE_HEIGHT + contentOffsetY; + + // Compute next panel placement (same as renderPlayingState) + const float NEXT_PANEL_WIDTH = GRID_W - finalBlockSize * 2.0f; + const float NEXT_PANEL_HEIGHT = finalBlockSize * 3.0f; + const float NEXT_PANEL_X = gridX + finalBlockSize; + // Move NEXT panel a bit higher so it visually separates from the grid + const float NEXT_PANEL_Y = gridY - NEXT_PANEL_HEIGHT - 12.0f; + + // We'll animate the piece that is now current (the newly spawned piece) + const Game::Piece piece = game->current(); + + // Determine piece bounds in its 4x4 to center into preview area + int minCx = 4, maxCx = -1, minCy = 4, maxCy = -1; + for (int cy = 0; cy < 4; ++cy) for (int cx = 0; cx < 4; ++cx) if (Game::cellFilled(piece, cx, cy)) { minCx = std::min(minCx, cx); maxCx = std::max(maxCx, cx); minCy = std::min(minCy, cy); maxCy = std::max(maxCy, cy); } + if (maxCx < minCx) { minCx = 0; maxCx = 0; } + if (maxCy < minCy) { minCy = 0; maxCy = 0; } + + const float labelReserve = finalBlockSize * 0.9f; + const float previewTop = NEXT_PANEL_Y + std::min(labelReserve, NEXT_PANEL_HEIGHT * 0.45f); + const float previewBottom = NEXT_PANEL_Y + NEXT_PANEL_HEIGHT - finalBlockSize * 0.25f; + const float previewCenterY = (previewTop + previewBottom) * 0.5f; + const float previewCenterX = std::round(NEXT_PANEL_X + NEXT_PANEL_WIDTH * 0.5f); + + const float pieceWidth = static_cast(maxCx - minCx + 1) * finalBlockSize; + const float pieceHeight = static_cast(maxCy - minCy + 1) * finalBlockSize; + float startX = previewCenterX - pieceWidth * 0.5f - static_cast(minCx) * finalBlockSize; + float startY = previewCenterY - pieceHeight * 0.5f - static_cast(minCy) * finalBlockSize; + // Snap to grid columns + float gridOriginX = NEXT_PANEL_X - finalBlockSize; + float rel = startX - gridOriginX; + float nearestTile = std::round(rel / finalBlockSize); + startX = gridOriginX + nearestTile * finalBlockSize; + startY = std::round(startY); + + // Target is the current piece's grid position + float targetX = gridX + piece.x * finalBlockSize; + float targetY = gridY + piece.y * finalBlockSize; + + // Also compute where the new NEXT preview (game->next()) will be drawn so we can fade it in later + const Game::Piece nextPiece = game->next(); + + // Compute next preview placement (center within NEXT panel) + int nMinCx = 4, nMaxCx = -1, nMinCy = 4, nMaxCy = -1; + for (int cy = 0; cy < 4; ++cy) for (int cx = 0; cx < 4; ++cx) if (Game::cellFilled(nextPiece, cx, cy)) { nMinCx = std::min(nMinCx, cx); nMaxCx = std::max(nMaxCx, cx); nMinCy = std::min(nMinCy, cy); nMaxCy = std::max(nMaxCy, cy); } + if (nMaxCx < nMinCx) { nMinCx = 0; nMaxCx = 0; } + if (nMaxCy < nMinCy) { nMinCy = 0; nMaxCy = 0; } + + const float previewTop2 = NEXT_PANEL_Y + std::min(finalBlockSize * 0.9f, NEXT_PANEL_HEIGHT * 0.45f); + const float previewBottom2 = NEXT_PANEL_Y + NEXT_PANEL_HEIGHT - finalBlockSize * 0.25f; + const float previewCenterY2 = (previewTop2 + previewBottom2) * 0.5f; + const float previewCenterX2 = std::round(NEXT_PANEL_X + NEXT_PANEL_WIDTH * 0.5f); + const float pieceWidth2 = static_cast(nMaxCx - nMinCx + 1) * finalBlockSize; + const float pieceHeight2 = static_cast(nMaxCy - nMinCy + 1) * finalBlockSize; + float nextPreviewX = previewCenterX2 - pieceWidth2 * 0.5f - static_cast(nMinCx) * finalBlockSize; + float nextPreviewY = previewCenterY2 - pieceHeight2 * 0.5f - static_cast(nMinCy) * finalBlockSize; + // Snap to grid columns + float gridOriginX2 = NEXT_PANEL_X - finalBlockSize; + float rel2 = nextPreviewX - gridOriginX2; + float nearestTile2 = std::round(rel2 / finalBlockSize); + nextPreviewX = gridOriginX2 + nearestTile2 * finalBlockSize; + nextPreviewY = std::round(nextPreviewY); + + // Initialize transport state to perform fades: preview fade-out -> grid fade-in -> next preview fade-in + s_transport.active = true; + s_transport.startTick = SDL_GetTicks(); + s_transport.durationMs = std::max(100.0f, durationSeconds * 1000.0f); + s_transport.piece = piece; + s_transport.startX = startX; + s_transport.startY = startY; + s_transport.targetX = targetX; + s_transport.targetY = targetY; + s_transport.tileSize = finalBlockSize; + // Store next preview piece and its pixel origin so we can fade it in later + s_transport.nextPiece = nextPiece; + s_transport.nextPreviewX = nextPreviewX; + s_transport.nextPreviewY = nextPreviewY; +} + +bool GameRenderer::isTransportActive() { + return s_transport.active; +} + // Draw the ongoing transport effect; called every frame from renderPlayingState static void updateAndDrawTransport(SDL_Renderer* renderer, SDL_Texture* blocksTex) { if (!s_transport.active) return; Uint32 now = SDL_GetTicks(); float elapsed = static_cast(now - s_transport.startTick); - float t = elapsed / s_transport.durationMs; - float eased = smoothstep(std::clamp(t, 0.0f, 1.0f)); + float total = s_transport.durationMs; + if (total <= 0.0f) total = 1.0f; + // Simultaneous cross-fade: as the NEXT preview fades out, the piece fades into the grid + // and the new NEXT preview fades in — all driven by the same normalized t in [0,1]. + float t = std::clamp(elapsed / total, 0.0f, 1.0f); + Uint8 previewAlpha = static_cast(std::lround(255.0f * (1.0f - t))); + Uint8 gridAlpha = static_cast(std::lround(255.0f * t)); + Uint8 nextAlpha = gridAlpha; // fade new NEXT preview in at same rate as grid - // Draw trailing particles / beam along the path - const int trailCount = 10; - for (int i = 0; i < trailCount; ++i) { - float p = eased - (static_cast(i) * 0.04f); - if (p <= 0.0f) continue; - p = std::clamp(p, 0.0f, 1.0f); - float px = std::lerp(s_transport.startX, s_transport.targetX, p); - float py = std::lerp(s_transport.startY, s_transport.targetY, p); - - // jitter for sci-fi shimmer - float jitter = static_cast(std::sin((now + i * 37) * 0.01f)) * (s_transport.tileSize * 0.06f); - SDL_FRect r{px + jitter, py - s_transport.tileSize * 0.06f, s_transport.tileSize * 0.18f, s_transport.tileSize * 0.18f}; - SDL_SetTextureColorMod(blocksTex, 255, 255, 255); - SDL_SetTextureAlphaMod(blocksTex, static_cast(std::clamp(255.0f * (0.5f * (1.0f - p)), 0.0f, 255.0f))); - GameRenderer::drawBlockTexturePublic(renderer, blocksTex, r.x, r.y, r.w, static_cast(s_transport.piece.type)); - } - // reset texture alpha to full - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); - - // Draw the piece itself at interpolated position between start and target - float curX = std::lerp(s_transport.startX, s_transport.targetX, eased); - float curY = std::lerp(s_transport.startY, s_transport.targetY, eased); - - // Render all filled cells of the piece at pixel coordinates - for (int cy = 0; cy < 4; ++cy) { - for (int cx = 0; cx < 4; ++cx) { - if (!Game::cellFilled(s_transport.piece, cx, cy)) continue; - float bx = curX + static_cast(cx) * s_transport.tileSize; - float by = curY + static_cast(cy) * s_transport.tileSize; - // pulse alpha while moving - float pulse = 0.6f + 0.4f * std::sin((now - s_transport.startTick) * 0.02f); - SDL_SetTextureAlphaMod(blocksTex, static_cast(std::clamp(255.0f * pulse * (1.0f - t), 0.0f, 255.0f))); - GameRenderer::drawBlockTexturePublic(renderer, blocksTex, bx, by, s_transport.tileSize, s_transport.piece.type); + // Draw preview fade-out + if (previewAlpha > 0) { + if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, previewAlpha); + for (int cy = 0; cy < 4; ++cy) { + for (int cx = 0; cx < 4; ++cx) { + if (!Game::cellFilled(s_transport.piece, cx, cy)) continue; + float px = s_transport.startX + static_cast(cx) * s_transport.tileSize; + float py = s_transport.startY + static_cast(cy) * s_transport.tileSize; + GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, s_transport.tileSize, s_transport.piece.type); + } } + if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); + } + + // Draw grid fade-in (same intensity as next preview fade-in) + if (gridAlpha > 0) { + if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, gridAlpha); + for (int cy = 0; cy < 4; ++cy) { + for (int cx = 0; cx < 4; ++cx) { + if (!Game::cellFilled(s_transport.piece, cx, cy)) continue; + float gx = s_transport.targetX + static_cast(cx) * s_transport.tileSize; + float gy = s_transport.targetY + static_cast(cy) * s_transport.tileSize; + GameRenderer::drawBlockTexturePublic(renderer, blocksTex, gx, gy, s_transport.tileSize, s_transport.piece.type); + } + } + if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); + } + + // Draw new NEXT preview fade-in (simultaneous) + if (nextAlpha > 0) { + if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, nextAlpha); + for (int cy = 0; cy < 4; ++cy) { + for (int cx = 0; cx < 4; ++cx) { + if (!Game::cellFilled(s_transport.nextPiece, cx, cy)) continue; + float nx = s_transport.nextPreviewX + static_cast(cx) * s_transport.tileSize; + float ny = s_transport.nextPreviewY + static_cast(cy) * s_transport.tileSize; + GameRenderer::drawBlockTexturePublic(renderer, blocksTex, nx, ny, s_transport.tileSize, s_transport.nextPiece.type); + } + } + if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); } - if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, 255); if (t >= 1.0f) { s_transport.active = false; @@ -350,14 +481,18 @@ void GameRenderer::renderNextPanel( // Round Y to pixel to avoid subpixel artifacts startY = std::round(startY); - for (int cy = 0; cy < 4; ++cy) { - for (int cx = 0; cx < 4; ++cx) { - if (!Game::cellFilled(nextPiece, cx, cy)) { - continue; + // If a transfer fade is active, the preview cells will be drawn by the + // transport effect (with fade). Skip drawing the normal preview in that case. + if (!s_transport.active) { + for (int cy = 0; cy < 4; ++cy) { + for (int cx = 0; cx < 4; ++cx) { + if (!Game::cellFilled(nextPiece, cx, cy)) { + continue; + } + const float px = startX + static_cast(cx) * tileSize; + const float py = startY + static_cast(cy) * tileSize; + GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, tileSize, nextPiece.type); } - const float px = startX + static_cast(cx) * tileSize; - const float py = startY + static_cast(cy) * tileSize; - GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, tileSize, nextPiece.type); } } } @@ -368,6 +503,7 @@ void GameRenderer::renderPlayingState( FontAtlas* pixelFont, LineEffect* lineEffect, SDL_Texture* blocksTex, + SDL_Texture* statisticsPanelTex, SDL_Texture* scorePanelTex, float logicalW, float logicalH, @@ -445,7 +581,8 @@ void GameRenderer::renderPlayingState( const float NEXT_PANEL_WIDTH = GRID_W - finalBlockSize * 2.0f; // leave 1 cell on left and right const float NEXT_PANEL_HEIGHT = finalBlockSize * 3.0f; const float NEXT_PANEL_X = gridX + finalBlockSize; // align panel so there's exactly one cell margin - const float NEXT_PANEL_Y = gridY - NEXT_PANEL_HEIGHT - 2.0f; // nudge up ~2px + // Move NEXT panel a bit higher so it visually separates from the grid + const float NEXT_PANEL_Y = gridY - NEXT_PANEL_HEIGHT - 12.0f; // nudge up ~12px // Handle line clearing effects if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) { @@ -478,7 +615,32 @@ void GameRenderer::renderPlayingState( statsW + blocksPanelPadLeft + blocksPanelPadRight, GRID_H + blocksPanelPadY * 2.0f }; - if (scorePanelTex) { + if (statisticsPanelTex) { + // Use the dedicated statistics panel image for the left panel when available. + // Preserve aspect ratio by scaling to the panel width and center/crop vertically if needed. + float texWf = 0.0f, texHf = 0.0f; + if (SDL_GetTextureSize(statisticsPanelTex, &texWf, &texHf) == 0) { + const float destW = blocksPanelBg.w; + const float destH = blocksPanelBg.h; + const float scale = destW / texWf; + const float scaledH = texHf * scale; + + if (scaledH <= destH) { + // Fits vertically: draw full texture centered vertically + SDL_FRect srcF{0.0f, 0.0f, texWf, texHf}; + SDL_RenderTexture(renderer, statisticsPanelTex, &srcF, &blocksPanelBg); + } else { + // Texture is taller when scaled to width: crop vertically from texture + float srcHf = destH / scale; + float srcYf = std::max(0.0f, (texHf - srcHf) * 0.5f); + SDL_FRect srcF{0.0f, srcYf, texWf, srcHf}; + SDL_RenderTexture(renderer, statisticsPanelTex, &srcF, &blocksPanelBg); + } + } else { + // Fallback: render entire texture if query failed + SDL_RenderTexture(renderer, statisticsPanelTex, nullptr, &blocksPanelBg); + } + } else if (scorePanelTex) { SDL_RenderTexture(renderer, scorePanelTex, nullptr, &blocksPanelBg); } else { SDL_SetRenderDrawColor(renderer, 12, 18, 32, 205); @@ -798,7 +960,7 @@ void GameRenderer::renderPlayingState( } } - bool allowActivePieceRender = true; + bool allowActivePieceRender = !GameRenderer::isTransportActive(); const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled(); float activePiecePixelOffsetX = 0.0f; @@ -919,86 +1081,150 @@ void GameRenderer::renderPlayingState( lineEffect->render(renderer, blocksTex, static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); } - // Draw block statistics (left panel) + // Draw block statistics (left panel) -> STATISTICS console const auto& blockCounts = game->getBlockCounts(); int totalBlocks = 0; for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i]; - const float rowPadding = 34.0f; - const float rowWidth = statsW - rowPadding * 2.0f; - const float rowSpacing = 18.0f; - float yCursor = statsY + 34.0f; + // Header (slightly smaller) + const SDL_Color headerColor{255, 220, 0, 255}; + const SDL_Color textColor{200, 220, 235, 200}; + const SDL_Color mutedColor{150, 180, 200, 180}; + pixelFont->draw(renderer, statsX + 12.0f, statsY + 8.0f, "STATISTICS", 0.92f, headerColor); + // Tighter spacing and smaller icons/text for compact analytics console + float yCursor = statsY + 36.0f; + const float leftPad = 12.0f; + const float rightPad = 14.0f; + // Increase row gap to avoid icon overlap on smaller scales + const float rowGap = 20.0f; + const float barHeight = 2.0f; + + // Determine max percent to highlight top used piece + int maxPerc = 0; for (int i = 0; i < PIECE_COUNT; ++i) { - float rowTop = yCursor; - float rowLeft = statsX + rowPadding; - float rowRight = rowLeft + rowWidth; - float previewSize = finalBlockSize * 0.5f; - float previewX = rowLeft; - float previewY = rowTop - 10.0f; - - 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 = rowRight - static_cast(countW); - float countY = previewY + 4.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 = rowLeft + previewSize + 36.0f; - float barY = previewY + pieceHeight + 10.0f; - float barH = 7.0f; - float barW = std::max(0.0f, rowRight - barX); - float percY = barY + barH + 6.0f; - - float rowBottom = percY + 18.0f; - SDL_FRect rowBg{ - rowLeft - 18.0f, - rowTop - 14.0f, - rowWidth + 36.0f, - rowBottom - (rowTop - 14.0f) - }; - SDL_SetRenderDrawColor(renderer, 6, 12, 26, 205); - SDL_RenderFillRect(renderer, &rowBg); - SDL_SetRenderDrawColor(renderer, 30, 60, 110, 220); - 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}); - - SDL_SetRenderDrawColor(renderer, 32, 44, 70, 210); - 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); - SDL_SetRenderDrawColor(renderer, 255, 255, 255, 45); - SDL_FRect fillHighlight{barX, barY, fillW, barH * 0.35f}; - SDL_RenderFillRect(renderer, &fillHighlight); - - pixelFont->draw(renderer, barX, percY, percStr, 0.78f, {185, 205, 230, 255}); - - yCursor = rowBottom + rowSpacing; + int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(blockCounts[i]) / double(totalBlocks))) : 0; + if (perc > maxPerc) maxPerc = perc; } + + // Row order groups: first 4, then last 3 + std::vector order = {0,1,2,3, 4,5,6}; + for (size_t idx = 0; idx < order.size(); ++idx) { + int i = order[idx]; + + float rowLeft = statsX + leftPad; + float rowRight = statsX + statsW - rightPad; + + // Icon card with a small backing to match the reference layout + float iconSize = finalBlockSize * 0.52f; + float iconBgPad = 6.0f; + float iconBgW = iconSize * 3.0f + iconBgPad * 2.0f; + float iconBgH = iconSize * 3.0f + iconBgPad * 2.0f; + float iconBgX = rowLeft - 6.0f; + float iconBgY = yCursor - 10.0f; + SDL_SetRenderDrawColor(renderer, 14, 20, 32, 210); + SDL_FRect iconBg{iconBgX, iconBgY, iconBgW, iconBgH}; + SDL_RenderFillRect(renderer, &iconBg); + SDL_SetRenderDrawColor(renderer, 40, 70, 110, 180); + SDL_RenderRect(renderer, &iconBg); + + // Measure right-side text first so we can vertically align icon with text + int count = blockCounts[i]; + char countStr[16]; snprintf(countStr, sizeof(countStr), "%dx", count); + int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(count) / double(totalBlocks))) : 0; + char percStr[16]; snprintf(percStr, sizeof(percStr), "%d%%", perc); + + int countW=0, countH=0; pixelFont->measure(countStr, 0.82f, countW, countH); + int percW=0, percH=0; pixelFont->measure(percStr, 0.78f, percW, percH); + + float iconX = iconBgX + iconBgPad; + float iconY = iconBgY + iconBgPad + 2.0f; + drawSmallPiece(renderer, blocksTex, static_cast(i), iconX, iconY, iconSize); + + // Badge for counts/percent so text sits on a soft dark backing + const float numbersGap = 14.0f; + const float numbersPadX = 10.0f; + const float numbersPadY = 6.0f; + int maxTextH = std::max(countH, percH); + float numbersW = numbersPadX * 2.0f + countW + numbersGap + percW; + float numbersH = numbersPadY * 2.0f + static_cast(maxTextH); + float numbersX = rowRight - numbersW; + float numbersY = yCursor - (numbersH - static_cast(maxTextH)) * 0.5f; + + SDL_SetRenderDrawColor(renderer, 32, 44, 60, 210); + SDL_FRect numbersBg{numbersX, numbersY, numbersW, numbersH}; + SDL_RenderFillRect(renderer, &numbersBg); + + float textY = numbersY + (numbersH - static_cast(maxTextH)) * 0.5f; + float countX = numbersX + numbersPadX; + float percX = numbersX + numbersW - percW - numbersPadX; + pixelFont->draw(renderer, countX, textY, countStr, 0.82f, textColor); + pixelFont->draw(renderer, percX, textY, percStr, 0.78f, mutedColor); + + // Progress bar anchored to the numbers badge, matching the reference width + float barX = numbersX; + float barW = numbersW; + float barY = numbersY + numbersH + 10.0f; + + SDL_SetRenderDrawColor(renderer, 24, 80, 120, 220); + SDL_FRect track{barX, barY, barW, barHeight}; + SDL_RenderFillRect(renderer, &track); + + // Fill color brightness based on usage and highlight for top piece + float strength = (totalBlocks > 0) ? (float(blockCounts[i]) / float(totalBlocks)) : 0.0f; + SDL_Color baseC = {60, 200, 255, 255}; + SDL_Color dimC = {40, 120, 160, 255}; + SDL_Color fillC = (perc == maxPerc) ? SDL_Color{100, 230, 255, 255} : SDL_Color{ + static_cast(std::lerp((float)dimC.r, (float)baseC.r, strength)), + static_cast(std::lerp((float)dimC.g, (float)baseC.g, strength)), + static_cast(std::lerp((float)dimC.b, (float)baseC.b, strength)), + 255 + }; + + float fillW = barW * std::clamp(strength, 0.0f, 1.0f); + SDL_SetRenderDrawColor(renderer, fillC.r, fillC.g, fillC.b, fillC.a); + SDL_FRect fill{barX, barY, fillW, barHeight}; + SDL_RenderFillRect(renderer, &fill); + + // Advance cursor to next row: after bar + gap (leave 20px space before next block) + yCursor = barY + barHeight + rowGap; + if (idx == 3) { + // faint separator + SDL_SetRenderDrawColor(renderer, 60, 80, 140, 90); + SDL_FRect sep{statsX + 8.0f, yCursor, statsW - 16.0f, 1.5f}; + SDL_RenderFillRect(renderer, &sep); + yCursor += 8.0f; + } + } + + // Bottom summary stats + float summaryY = statsY + statsH - 90.0f; // move summary slightly up + const SDL_Color summaryValueColor{220, 235, 250, 255}; + const SDL_Color labelMuted{160, 180, 200, 200}; + + char totalStr[32]; snprintf(totalStr, sizeof(totalStr), "%d", totalBlocks); + char tetrisesStr[32]; snprintf(tetrisesStr, sizeof(tetrisesStr), "%d", game->tetrisesMade()); + char maxComboStr[32]; snprintf(maxComboStr, sizeof(maxComboStr), "%d", game->maxCombo()); + + // Use slightly smaller labels/values to match the compact look + const float labelX = statsX + 8.0f; // move labels more left + const float valueRightPad = 12.0f; // pad from right edge + + int valW=0, valH=0; + pixelFont->measure(totalStr, 0.82f, valW, valH); + float totalX = statsX + statsW - valueRightPad - (float)valW; + pixelFont->draw(renderer, labelX, summaryY + 0.0f, "TOTAL PIECES", 0.72f, labelMuted); + pixelFont->draw(renderer, totalX, summaryY + 0.0f, totalStr, 0.82f, summaryValueColor); + + pixelFont->measure(tetrisesStr, 0.82f, valW, valH); + float tetrisesX = statsX + statsW - valueRightPad - (float)valW; + pixelFont->draw(renderer, labelX, summaryY + 22.0f, "TETRISES MADE", 0.72f, labelMuted); + pixelFont->draw(renderer, tetrisesX, summaryY + 22.0f, tetrisesStr, 0.82f, summaryValueColor); + + pixelFont->measure(maxComboStr, 0.82f, valW, valH); + float comboX = statsX + statsW - valueRightPad - (float)valW; + pixelFont->draw(renderer, labelX, summaryY + 44.0f, "MAX COMBO", 0.72f, labelMuted); + pixelFont->draw(renderer, comboX, summaryY + 44.0f, maxComboStr, 0.82f, summaryValueColor); // Draw score panel (right side) const float contentTopOffset = 0.0f; diff --git a/src/graphics/renderers/GameRenderer.h b/src/graphics/renderers/GameRenderer.h index cadccb5..33b4654 100644 --- a/src/graphics/renderers/GameRenderer.h +++ b/src/graphics/renderers/GameRenderer.h @@ -21,6 +21,7 @@ public: FontAtlas* pixelFont, LineEffect* lineEffect, SDL_Texture* blocksTex, + SDL_Texture* statisticsPanelTex, SDL_Texture* scorePanelTex, float logicalW, float logicalH, @@ -52,6 +53,16 @@ public: // calling from non-member helper functions (e.g. visual effects) that cannot // access private class members. static void drawBlockTexturePublic(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType); + // Transport/teleport visual effect API (public): start a sci-fi "transport" animation + // moving a visual copy of `piece` from screen pixel origin (startX,startY) to + // target pixel origin (targetX,targetY). `tileSize` should be the same cell size + // used for the grid. Duration is seconds. + static void startTransportEffect(const Game::Piece& piece, float startX, float startY, float targetX, float targetY, float tileSize, float durationSeconds = 0.6f); + // Convenience: compute the preview & grid positions using the same layout math + // used by `renderPlayingState` and start the transport effect for the current + // `game` using renderer layout parameters. + static void startTransportEffectForGame(Game* game, SDL_Texture* blocksTex, float logicalW, float logicalH, float logicalScale, float winW, float winH, float durationSeconds = 0.6f); + static bool isTransportActive(); private: // Helper functions for drawing game elements @@ -59,11 +70,6 @@ private: static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false, float pixelOffsetX = 0.0f, float pixelOffsetY = 0.0f); static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize); static void renderNextPanel(SDL_Renderer* renderer, FontAtlas* pixelFont, SDL_Texture* blocksTex, const Game::Piece& nextPiece, float panelX, float panelY, float panelW, float panelH, float tileSize); - // Transport/teleport visual effect: start a sci-fi "transport" animation moving - // a visual copy of `piece` from screen pixel origin (startX,startY) to - // target pixel origin (targetX,targetY). `tileSize` should be the same cell size - // used for the grid. Duration is seconds. - static void startTransportEffect(const Game::Piece& piece, float startX, float startY, float targetX, float targetY, float tileSize, float durationSeconds = 0.6f); // Helper function for drawing rectangles static void drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c); diff --git a/src/main.cpp b/src/main.cpp index d8d571a..bda5a62 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -706,6 +706,10 @@ int main(int, char **) if (scorePanelTex) { SDL_SetTextureBlendMode(scorePanelTex, SDL_BLENDMODE_BLEND); } + SDL_Texture* statisticsPanelTex = loadTextureFromImage(renderer, "assets/images/statistics_panel.png"); + if (statisticsPanelTex) { + SDL_SetTextureBlendMode(statisticsPanelTex, SDL_BLENDMODE_BLEND); + } Game game(startLevelSelection); // Apply global gravity speed multiplier from config @@ -854,6 +858,7 @@ int main(int, char **) ctx.backgroundTex = backgroundTex; ctx.blocksTex = blocksTex; ctx.scorePanelTex = scorePanelTex; + ctx.statisticsPanelTex = statisticsPanelTex; ctx.mainScreenTex = mainScreenTex; ctx.mainScreenW = mainScreenW; ctx.mainScreenH = mainScreenH; @@ -1753,6 +1758,7 @@ int main(int, char **) &pixelFont, &lineEffect, blocksTex, + ctx.statisticsPanelTex, scorePanelTex, (float)LOGICAL_W, (float)LOGICAL_H, diff --git a/src/states/PlayingState.cpp b/src/states/PlayingState.cpp index 08db8fe..35a59f6 100644 --- a/src/states/PlayingState.cpp +++ b/src/states/PlayingState.cpp @@ -9,6 +9,10 @@ #include "../core/Config.h" #include +// File-scope transport/spawn detection state +static uint64_t s_lastPieceSequence = 0; +static bool s_pendingTransport = false; + PlayingState::PlayingState(StateContext& ctx) : State(ctx) {} void PlayingState::onEnter() { @@ -18,6 +22,12 @@ void PlayingState::onEnter() { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection); ctx.game->reset(*ctx.startLevelSelection); } + if (ctx.game) { + s_lastPieceSequence = ctx.game->getCurrentPieceSequence(); + s_pendingTransport = false; + } + + // (transport state is tracked at file scope) } void PlayingState::onExit() { @@ -28,6 +38,10 @@ void PlayingState::onExit() { } void PlayingState::handleEvent(const SDL_Event& e) { + // If a transport animation is active, ignore gameplay input entirely. + if (GameRenderer::isTransportActive()) { + return; + } // We keep short-circuited input here; main still owns mouse UI if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { if (!ctx.game) return; @@ -130,10 +144,21 @@ void PlayingState::update(double frameMs) { if (!ctx.game) return; ctx.game->updateVisualEffects(frameMs); + // If a transport animation is active, pause gameplay updates and ignore inputs + if (GameRenderer::isTransportActive()) { + // Keep visual effects updating but skip gravity/timers while transport runs + return; + } // forward per-frame gameplay updates (gravity, line effects) if (!ctx.game->isPaused()) { ctx.game->tickGravity(frameMs); + // Detect spawn event (sequence increment) and request transport effect + uint64_t seq = ctx.game->getCurrentPieceSequence(); + if (seq != s_lastPieceSequence) { + s_lastPieceSequence = seq; + s_pendingTransport = true; + } ctx.game->updateElapsedTime(); if (ctx.lineEffect && ctx.lineEffect->isActive()) { @@ -183,12 +208,20 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l SDL_SetRenderScale(renderer, logicalScale, logicalScale); // Render game content (no overlays) + // If a transport effect was requested due to a recent spawn, start it here so + // the renderer has the correct layout and renderer context to compute coords. + if (s_pendingTransport) { + GameRenderer::startTransportEffectForGame(ctx.game, ctx.blocksTex, 1200.0f, 1000.0f, logicalScale, (float)winW, (float)winH, 0.4f); + s_pendingTransport = false; + } + GameRenderer::renderPlayingState( renderer, ctx.game, ctx.pixelFont, ctx.lineEffect, ctx.blocksTex, + ctx.statisticsPanelTex, ctx.scorePanelTex, 1200.0f, // LOGICAL_W 1000.0f, // LOGICAL_H @@ -264,12 +297,17 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l } else { // Render normally directly to screen + if (s_pendingTransport) { + GameRenderer::startTransportEffectForGame(ctx.game, ctx.blocksTex, 1200.0f, 1000.0f, logicalScale, (float)winW, (float)winH, 0.4f); + s_pendingTransport = false; + } GameRenderer::renderPlayingState( renderer, ctx.game, ctx.pixelFont, ctx.lineEffect, ctx.blocksTex, + ctx.statisticsPanelTex, ctx.scorePanelTex, 1200.0f, 1000.0f, diff --git a/src/states/State.h b/src/states/State.h index 7271044..cc053c8 100644 --- a/src/states/State.h +++ b/src/states/State.h @@ -41,6 +41,7 @@ struct StateContext { // Prefer reading this field instead of relying on any `extern SDL_Texture*` globals. SDL_Texture* blocksTex = nullptr; SDL_Texture* scorePanelTex = nullptr; + SDL_Texture* statisticsPanelTex = nullptr; SDL_Texture* mainScreenTex = nullptr; int mainScreenW = 0; int mainScreenH = 0;