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.pixelFont,
|
||||||
m_stateContext.lineEffect,
|
m_stateContext.lineEffect,
|
||||||
m_stateContext.blocksTex,
|
m_stateContext.blocksTex,
|
||||||
|
m_stateContext.statisticsPanelTex,
|
||||||
m_stateContext.scorePanelTex,
|
m_stateContext.scorePanelTex,
|
||||||
LOGICAL_W,
|
LOGICAL_W,
|
||||||
LOGICAL_H,
|
LOGICAL_H,
|
||||||
|
|||||||
@ -55,6 +55,9 @@ void Game::reset(int startLevel_) {
|
|||||||
std::fill(blockCounts.begin(), blockCounts.end(), 0);
|
std::fill(blockCounts.begin(), blockCounts.end(), 0);
|
||||||
bag.clear();
|
bag.clear();
|
||||||
_score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_;
|
_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)
|
// Initialize gravity using NES timing table (ms per cell by level)
|
||||||
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
|
||||||
fallAcc = 0; gameOver=false; paused=false;
|
fallAcc = 0; gameOver=false; paused=false;
|
||||||
@ -218,6 +221,15 @@ void Game::lockPiece() {
|
|||||||
// Update total lines
|
// Update total lines
|
||||||
_lines += cleared;
|
_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
|
// JS level progression (NES-like) using starting level rules
|
||||||
// Both startLevel and _level are 0-based now.
|
// Both startLevel and _level are 0-based now.
|
||||||
int targetLevel = startLevel;
|
int targetLevel = startLevel;
|
||||||
@ -242,7 +254,10 @@ void Game::lockPiece() {
|
|||||||
soundCallback(cleared);
|
soundCallback(cleared);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// No clear -> reset combo
|
||||||
|
_currentCombo = 0;
|
||||||
|
}
|
||||||
if (!gameOver) spawn();
|
if (!gameOver) spawn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,9 @@ public:
|
|||||||
const std::vector<SDL_Point>& getHardDropCells() const { return hardDropCells; }
|
const std::vector<SDL_Point>& getHardDropCells() const { return hardDropCells; }
|
||||||
uint32_t getHardDropFxId() const { return hardDropFxId; }
|
uint32_t getHardDropFxId() const { return hardDropFxId; }
|
||||||
uint64_t getCurrentPieceSequence() const { return pieceSequence; }
|
uint64_t getCurrentPieceSequence() const { return pieceSequence; }
|
||||||
|
// Additional stats
|
||||||
|
int tetrisesMade() const { return _tetrisesMade; }
|
||||||
|
int maxCombo() const { return _maxCombo; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
|
std::array<int, COLS*ROWS> board{}; // 0 empty else color index
|
||||||
@ -94,6 +97,9 @@ private:
|
|||||||
int _score{0};
|
int _score{0};
|
||||||
int _lines{0};
|
int _lines{0};
|
||||||
int _level{1};
|
int _level{1};
|
||||||
|
int _tetrisesMade{0};
|
||||||
|
int _currentCombo{0};
|
||||||
|
int _maxCombo{0};
|
||||||
double gravityMs{800.0};
|
double gravityMs{800.0};
|
||||||
double fallAcc{0.0};
|
double fallAcc{0.0};
|
||||||
Uint64 _startTime{0}; // Performance counter at game start
|
Uint64 _startTime{0}; // Performance counter at game start
|
||||||
|
|||||||
@ -61,6 +61,10 @@ struct TransportEffectState {
|
|||||||
float targetX = 0.0f;
|
float targetX = 0.0f;
|
||||||
float targetY = 0.0f;
|
float targetY = 0.0f;
|
||||||
float tileSize = 24.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;
|
static TransportEffectState s_transport;
|
||||||
@ -82,50 +86,177 @@ void GameRenderer::startTransportEffect(const Game::Piece& piece, float startX,
|
|||||||
s_transport.tileSize = tileSize;
|
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
|
// Draw the ongoing transport effect; called every frame from renderPlayingState
|
||||||
static void updateAndDrawTransport(SDL_Renderer* renderer, SDL_Texture* blocksTex) {
|
static void updateAndDrawTransport(SDL_Renderer* renderer, SDL_Texture* blocksTex) {
|
||||||
if (!s_transport.active) return;
|
if (!s_transport.active) return;
|
||||||
Uint32 now = SDL_GetTicks();
|
Uint32 now = SDL_GetTicks();
|
||||||
float elapsed = static_cast<float>(now - s_transport.startTick);
|
float elapsed = static_cast<float>(now - s_transport.startTick);
|
||||||
float t = elapsed / s_transport.durationMs;
|
float total = s_transport.durationMs;
|
||||||
float eased = smoothstep(std::clamp(t, 0.0f, 1.0f));
|
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
|
// Draw preview fade-out
|
||||||
const int trailCount = 10;
|
if (previewAlpha > 0) {
|
||||||
for (int i = 0; i < trailCount; ++i) {
|
if (blocksTex) SDL_SetTextureAlphaMod(blocksTex, previewAlpha);
|
||||||
float p = eased - (static_cast<float>(i) * 0.04f);
|
for (int cy = 0; cy < 4; ++cy) {
|
||||||
if (p <= 0.0f) continue;
|
for (int cx = 0; cx < 4; ++cx) {
|
||||||
p = std::clamp(p, 0.0f, 1.0f);
|
if (!Game::cellFilled(s_transport.piece, cx, cy)) continue;
|
||||||
float px = std::lerp(s_transport.startX, s_transport.targetX, p);
|
float px = s_transport.startX + static_cast<float>(cx) * s_transport.tileSize;
|
||||||
float py = std::lerp(s_transport.startY, s_transport.targetY, p);
|
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);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
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) {
|
if (t >= 1.0f) {
|
||||||
s_transport.active = false;
|
s_transport.active = false;
|
||||||
@ -350,14 +481,18 @@ void GameRenderer::renderNextPanel(
|
|||||||
// Round Y to pixel to avoid subpixel artifacts
|
// Round Y to pixel to avoid subpixel artifacts
|
||||||
startY = std::round(startY);
|
startY = std::round(startY);
|
||||||
|
|
||||||
for (int cy = 0; cy < 4; ++cy) {
|
// If a transfer fade is active, the preview cells will be drawn by the
|
||||||
for (int cx = 0; cx < 4; ++cx) {
|
// transport effect (with fade). Skip drawing the normal preview in that case.
|
||||||
if (!Game::cellFilled(nextPiece, cx, cy)) {
|
if (!s_transport.active) {
|
||||||
continue;
|
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,
|
FontAtlas* pixelFont,
|
||||||
LineEffect* lineEffect,
|
LineEffect* lineEffect,
|
||||||
SDL_Texture* blocksTex,
|
SDL_Texture* blocksTex,
|
||||||
|
SDL_Texture* statisticsPanelTex,
|
||||||
SDL_Texture* scorePanelTex,
|
SDL_Texture* scorePanelTex,
|
||||||
float logicalW,
|
float logicalW,
|
||||||
float logicalH,
|
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_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_HEIGHT = finalBlockSize * 3.0f;
|
||||||
const float NEXT_PANEL_X = gridX + finalBlockSize; // align panel so there's exactly one cell margin
|
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
|
// Handle line clearing effects
|
||||||
if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) {
|
if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) {
|
||||||
@ -478,7 +615,32 @@ void GameRenderer::renderPlayingState(
|
|||||||
statsW + blocksPanelPadLeft + blocksPanelPadRight,
|
statsW + blocksPanelPadLeft + blocksPanelPadRight,
|
||||||
GRID_H + blocksPanelPadY * 2.0f
|
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);
|
SDL_RenderTexture(renderer, scorePanelTex, nullptr, &blocksPanelBg);
|
||||||
} else {
|
} else {
|
||||||
SDL_SetRenderDrawColor(renderer, 12, 18, 32, 205);
|
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();
|
const bool smoothScrollEnabled = Settings::instance().isSmoothScrollEnabled();
|
||||||
|
|
||||||
float activePiecePixelOffsetX = 0.0f;
|
float activePiecePixelOffsetX = 0.0f;
|
||||||
@ -919,87 +1081,151 @@ void GameRenderer::renderPlayingState(
|
|||||||
lineEffect->render(renderer, blocksTex, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
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();
|
const auto& blockCounts = game->getBlockCounts();
|
||||||
int totalBlocks = 0;
|
int totalBlocks = 0;
|
||||||
for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i];
|
for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i];
|
||||||
|
|
||||||
const float rowPadding = 34.0f;
|
// Header (slightly smaller)
|
||||||
const float rowWidth = statsW - rowPadding * 2.0f;
|
const SDL_Color headerColor{255, 220, 0, 255};
|
||||||
const float rowSpacing = 18.0f;
|
const SDL_Color textColor{200, 220, 235, 200};
|
||||||
float yCursor = statsY + 34.0f;
|
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) {
|
for (int i = 0; i < PIECE_COUNT; ++i) {
|
||||||
float rowTop = yCursor;
|
int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(blockCounts[i]) / double(totalBlocks))) : 0;
|
||||||
float rowLeft = statsX + rowPadding;
|
if (perc > maxPerc) maxPerc = perc;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
// Draw score panel (right side)
|
||||||
const float contentTopOffset = 0.0f;
|
const float contentTopOffset = 0.0f;
|
||||||
const float contentBottomOffset = 290.0f;
|
const float contentBottomOffset = 290.0f;
|
||||||
|
|||||||
@ -21,6 +21,7 @@ public:
|
|||||||
FontAtlas* pixelFont,
|
FontAtlas* pixelFont,
|
||||||
LineEffect* lineEffect,
|
LineEffect* lineEffect,
|
||||||
SDL_Texture* blocksTex,
|
SDL_Texture* blocksTex,
|
||||||
|
SDL_Texture* statisticsPanelTex,
|
||||||
SDL_Texture* scorePanelTex,
|
SDL_Texture* scorePanelTex,
|
||||||
float logicalW,
|
float logicalW,
|
||||||
float logicalH,
|
float logicalH,
|
||||||
@ -52,6 +53,16 @@ public:
|
|||||||
// calling from non-member helper functions (e.g. visual effects) that cannot
|
// calling from non-member helper functions (e.g. visual effects) that cannot
|
||||||
// access private class members.
|
// access private class members.
|
||||||
static void drawBlockTexturePublic(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType);
|
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:
|
private:
|
||||||
// Helper functions for drawing game elements
|
// 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 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 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);
|
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
|
// Helper function for drawing rectangles
|
||||||
static void drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c);
|
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) {
|
if (scorePanelTex) {
|
||||||
SDL_SetTextureBlendMode(scorePanelTex, SDL_BLENDMODE_BLEND);
|
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);
|
Game game(startLevelSelection);
|
||||||
// Apply global gravity speed multiplier from config
|
// Apply global gravity speed multiplier from config
|
||||||
@ -854,6 +858,7 @@ int main(int, char **)
|
|||||||
ctx.backgroundTex = backgroundTex;
|
ctx.backgroundTex = backgroundTex;
|
||||||
ctx.blocksTex = blocksTex;
|
ctx.blocksTex = blocksTex;
|
||||||
ctx.scorePanelTex = scorePanelTex;
|
ctx.scorePanelTex = scorePanelTex;
|
||||||
|
ctx.statisticsPanelTex = statisticsPanelTex;
|
||||||
ctx.mainScreenTex = mainScreenTex;
|
ctx.mainScreenTex = mainScreenTex;
|
||||||
ctx.mainScreenW = mainScreenW;
|
ctx.mainScreenW = mainScreenW;
|
||||||
ctx.mainScreenH = mainScreenH;
|
ctx.mainScreenH = mainScreenH;
|
||||||
@ -1753,6 +1758,7 @@ int main(int, char **)
|
|||||||
&pixelFont,
|
&pixelFont,
|
||||||
&lineEffect,
|
&lineEffect,
|
||||||
blocksTex,
|
blocksTex,
|
||||||
|
ctx.statisticsPanelTex,
|
||||||
scorePanelTex,
|
scorePanelTex,
|
||||||
(float)LOGICAL_W,
|
(float)LOGICAL_W,
|
||||||
(float)LOGICAL_H,
|
(float)LOGICAL_H,
|
||||||
|
|||||||
@ -9,6 +9,10 @@
|
|||||||
#include "../core/Config.h"
|
#include "../core/Config.h"
|
||||||
#include <SDL3/SDL.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) {}
|
PlayingState::PlayingState(StateContext& ctx) : State(ctx) {}
|
||||||
|
|
||||||
void PlayingState::onEnter() {
|
void PlayingState::onEnter() {
|
||||||
@ -18,6 +22,12 @@ void PlayingState::onEnter() {
|
|||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection);
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection);
|
||||||
ctx.game->reset(*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() {
|
void PlayingState::onExit() {
|
||||||
@ -28,6 +38,10 @@ void PlayingState::onExit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PlayingState::handleEvent(const SDL_Event& e) {
|
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
|
// We keep short-circuited input here; main still owns mouse UI
|
||||||
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||||
if (!ctx.game) return;
|
if (!ctx.game) return;
|
||||||
@ -130,10 +144,21 @@ void PlayingState::update(double frameMs) {
|
|||||||
if (!ctx.game) return;
|
if (!ctx.game) return;
|
||||||
|
|
||||||
ctx.game->updateVisualEffects(frameMs);
|
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)
|
// forward per-frame gameplay updates (gravity, line effects)
|
||||||
if (!ctx.game->isPaused()) {
|
if (!ctx.game->isPaused()) {
|
||||||
ctx.game->tickGravity(frameMs);
|
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();
|
ctx.game->updateElapsedTime();
|
||||||
|
|
||||||
if (ctx.lineEffect && ctx.lineEffect->isActive()) {
|
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);
|
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
|
||||||
|
|
||||||
// Render game content (no overlays)
|
// 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(
|
GameRenderer::renderPlayingState(
|
||||||
renderer,
|
renderer,
|
||||||
ctx.game,
|
ctx.game,
|
||||||
ctx.pixelFont,
|
ctx.pixelFont,
|
||||||
ctx.lineEffect,
|
ctx.lineEffect,
|
||||||
ctx.blocksTex,
|
ctx.blocksTex,
|
||||||
|
ctx.statisticsPanelTex,
|
||||||
ctx.scorePanelTex,
|
ctx.scorePanelTex,
|
||||||
1200.0f, // LOGICAL_W
|
1200.0f, // LOGICAL_W
|
||||||
1000.0f, // LOGICAL_H
|
1000.0f, // LOGICAL_H
|
||||||
@ -264,12 +297,17 @@ void PlayingState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect l
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Render normally directly to screen
|
// 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(
|
GameRenderer::renderPlayingState(
|
||||||
renderer,
|
renderer,
|
||||||
ctx.game,
|
ctx.game,
|
||||||
ctx.pixelFont,
|
ctx.pixelFont,
|
||||||
ctx.lineEffect,
|
ctx.lineEffect,
|
||||||
ctx.blocksTex,
|
ctx.blocksTex,
|
||||||
|
ctx.statisticsPanelTex,
|
||||||
ctx.scorePanelTex,
|
ctx.scorePanelTex,
|
||||||
1200.0f,
|
1200.0f,
|
||||||
1000.0f,
|
1000.0f,
|
||||||
|
|||||||
@ -41,6 +41,7 @@ struct StateContext {
|
|||||||
// Prefer reading this field instead of relying on any `extern SDL_Texture*` globals.
|
// Prefer reading this field instead of relying on any `extern SDL_Texture*` globals.
|
||||||
SDL_Texture* blocksTex = nullptr;
|
SDL_Texture* blocksTex = nullptr;
|
||||||
SDL_Texture* scorePanelTex = nullptr;
|
SDL_Texture* scorePanelTex = nullptr;
|
||||||
|
SDL_Texture* statisticsPanelTex = nullptr;
|
||||||
SDL_Texture* mainScreenTex = nullptr;
|
SDL_Texture* mainScreenTex = nullptr;
|
||||||
int mainScreenW = 0;
|
int mainScreenW = 0;
|
||||||
int mainScreenH = 0;
|
int mainScreenH = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user