fixed statistics

This commit is contained in:
2025-12-07 17:30:18 +01:00
parent 24779755a5
commit 2b4b07ae6a
10 changed files with 424 additions and 125 deletions

View File

@ -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<float>(maxCx - minCx + 1) * finalBlockSize;
const float pieceHeight = static_cast<float>(maxCy - minCy + 1) * finalBlockSize;
float startX = previewCenterX - pieceWidth * 0.5f - static_cast<float>(minCx) * finalBlockSize;
float startY = previewCenterY - pieceHeight * 0.5f - static_cast<float>(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<float>(nMaxCx - nMinCx + 1) * finalBlockSize;
const float pieceHeight2 = static_cast<float>(nMaxCy - nMinCy + 1) * finalBlockSize;
float nextPreviewX = previewCenterX2 - pieceWidth2 * 0.5f - static_cast<float>(nMinCx) * finalBlockSize;
float nextPreviewY = previewCenterY2 - pieceHeight2 * 0.5f - static_cast<float>(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<float>(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<Uint8>(std::lround(255.0f * (1.0f - t)));
Uint8 gridAlpha = static_cast<Uint8>(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<float>(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<float>(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<Uint8>(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<int>(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<float>(cx) * s_transport.tileSize;
float by = curY + static_cast<float>(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<Uint8>(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<float>(cx) * s_transport.tileSize;
float py = s_transport.startY + static_cast<float>(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<float>(cx) * s_transport.tileSize;
float gy = s_transport.targetY + static_cast<float>(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<float>(cx) * s_transport.tileSize;
float ny = s_transport.nextPreviewY + static_cast<float>(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<float>(cx) * tileSize;
const float py = startY + static_cast<float>(cy) * tileSize;
GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, tileSize, nextPiece.type);
}
const float px = startX + static_cast<float>(cx) * tileSize;
const float py = startY + static_cast<float>(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<int>(gridX), static_cast<int>(gridY), static_cast<int>(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<PieceType>(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<float>(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<PieceType>(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<int> 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<PieceType>(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<float>(maxTextH);
float numbersX = rowRight - numbersW;
float numbersY = yCursor - (numbersH - static_cast<float>(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<float>(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<Uint8>(std::lerp((float)dimC.r, (float)baseC.r, strength)),
static_cast<Uint8>(std::lerp((float)dimC.g, (float)baseC.g, strength)),
static_cast<Uint8>(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;

View File

@ -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);