fixed statistics
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 275 KiB |
BIN
assets/images/statistics_panel.png
Normal file
BIN
assets/images/statistics_panel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 513 KiB |
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)) {
|
||||
@ -361,6 +495,7 @@ void GameRenderer::renderNextPanel(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GameRenderer::renderPlayingState(
|
||||
SDL_Renderer* renderer,
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user