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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

View File

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

View File

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

View File

@ -81,6 +81,9 @@ public:
const std::vector<SDL_Point>& 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<int, COLS*ROWS> 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

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
// 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 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);
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 (t >= 1.0f) {
s_transport.active = false;
@ -350,6 +481,9 @@ void GameRenderer::renderNextPanel(
// Round Y to pixel to avoid subpixel artifacts
startY = std::round(startY);
// 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)) {
@ -360,6 +494,7 @@ void GameRenderer::renderNextPanel(
GameRenderer::drawBlockTexturePublic(renderer, blocksTex, px, py, tileSize, nextPiece.type);
}
}
}
}
void GameRenderer::renderPlayingState(
@ -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;
int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(blockCounts[i]) / double(totalBlocks))) : 0;
if (perc > maxPerc) maxPerc = perc;
}
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;
// 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), "%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;
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);
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;
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 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);
float iconX = iconBgX + iconBgPad;
float iconY = iconBgY + iconBgPad + 2.0f;
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(i), iconX, iconY, iconSize);
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(i), previewX, previewY, previewSize);
pixelFont->draw(renderer, countX, countY, countStr, 1.0f, {245, 245, 255, 255});
// 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, 70, 210);
SDL_FRect track{barX, barY, barW, barH};
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);
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};
// 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);
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;
// 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);

View File

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

View File

@ -9,6 +9,10 @@
#include "../core/Config.h"
#include <SDL3/SDL.h>
// 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,

View File

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