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