feat: Add Firebase high score sync, menu music, and gameplay improvements
- Integrate Firebase Realtime Database for high score synchronization - Add cpr and nlohmann-json dependencies for HTTP requests - Implement async score loading from Firebase with local fallback - Submit all scores > 0 to Firebase in background thread - Always prompt for player name on game over if score > 0 - Add dedicated menu music system - Implement menu track support in Audio class with looping - Add "Every Block You Take.mp3" as main menu theme - Automatically switch between menu and game music on state transitions - Load menu track asynchronously to prevent startup delays - Update level speed progression to match web version - Replace NES frame-based gravity with explicit millisecond values - Implement 20-level speed table (1000ms to 60ms) - Ensure consistent gameplay between C++ and web versions - Fix startup performance issues - Move score loading to background thread to prevent UI freeze - Optimize Firebase network requests with 2s timeout - Add graceful fallback to local scores on network failure Files modified: - src/persistence/Scores.cpp/h - Firebase integration - src/audio/Audio.cpp/h - Menu music support - src/core/GravityManager.cpp/h - Level speed updates - src/main.cpp - State-based music switching, async loading - CMakeLists.txt - Add cpr and nlohmann-json dependencies - vcpkg.json - Update dependency list
This commit is contained in:
591
src/main.cpp
591
src/main.cpp
@ -29,6 +29,7 @@
|
||||
#include "states/LevelSelectorState.h"
|
||||
#include "states/PlayingState.h"
|
||||
#include "audio/MenuWrappers.h"
|
||||
#include "graphics/renderers/GameRenderer.h"
|
||||
|
||||
// Debug logging removed: no-op in this build (previously LOG_DEBUG)
|
||||
|
||||
@ -278,6 +279,8 @@ static bool showSettingsPopup = false;
|
||||
static bool showExitConfirmPopup = false;
|
||||
static bool musicEnabled = true;
|
||||
static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings
|
||||
static bool isNewHighScore = false;
|
||||
static std::string playerName = "";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Tetris Block Fireworks for intro animation (block particles)
|
||||
@ -437,7 +440,10 @@ int main(int, char **)
|
||||
pixelFont.init("assets/fonts/PressStart2P-Regular.ttf", 16);
|
||||
|
||||
ScoreManager scores;
|
||||
scores.load();
|
||||
// Load scores asynchronously to prevent startup hang due to network request
|
||||
std::thread([&scores]() {
|
||||
scores.load();
|
||||
}).detach();
|
||||
Starfield starfield;
|
||||
starfield.init(200, LOGICAL_W, LOGICAL_H);
|
||||
Starfield3D starfield3D;
|
||||
@ -726,6 +732,37 @@ int main(int, char **)
|
||||
SDL_SetWindowFullscreen(window, isFullscreen ? SDL_WINDOW_FULLSCREEN : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Text input for high score
|
||||
if (state == AppState::GameOver && isNewHighScore && e.type == SDL_EVENT_TEXT_INPUT) {
|
||||
if (playerName.length() < 12) {
|
||||
playerName += e.text.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (state == AppState::GameOver && e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||
if (isNewHighScore) {
|
||||
if (e.key.scancode == SDL_SCANCODE_BACKSPACE && !playerName.empty()) {
|
||||
playerName.pop_back();
|
||||
} else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
|
||||
if (playerName.empty()) playerName = "PLAYER";
|
||||
scores.submit(game.score(), game.lines(), game.level(), game.elapsed(), playerName);
|
||||
isNewHighScore = false;
|
||||
SDL_StopTextInput(window);
|
||||
}
|
||||
} else {
|
||||
if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) {
|
||||
// Restart
|
||||
game.reset(startLevelSelection);
|
||||
state = AppState::Playing;
|
||||
stateMgr.setState(state);
|
||||
} else if (e.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
// Menu
|
||||
state = AppState::Menu;
|
||||
stateMgr.setState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse handling remains in main loop for UI interactions
|
||||
if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN)
|
||||
@ -923,7 +960,15 @@ int main(int, char **)
|
||||
}
|
||||
if (game.isGameOver())
|
||||
{
|
||||
scores.submit(game.score(), game.lines(), game.level(), game.elapsed());
|
||||
// Always allow name entry if score > 0
|
||||
if (game.score() > 0) {
|
||||
isNewHighScore = true; // Reuse flag to trigger input mode
|
||||
playerName = "";
|
||||
SDL_StartTextInput(window);
|
||||
} else {
|
||||
isNewHighScore = false;
|
||||
scores.submit(game.score(), game.lines(), game.level(), game.elapsed());
|
||||
}
|
||||
state = AppState::GameOver;
|
||||
stateMgr.setState(state);
|
||||
}
|
||||
@ -1006,12 +1051,38 @@ int main(int, char **)
|
||||
{
|
||||
if (!musicStarted && musicLoaded)
|
||||
{
|
||||
// Music tracks are already loaded during loading screen, just start playback
|
||||
Audio::instance().start();
|
||||
// Load menu track once on first menu entry (in background to avoid blocking)
|
||||
static bool menuTrackLoaded = false;
|
||||
if (!menuTrackLoaded) {
|
||||
std::thread([]() {
|
||||
Audio::instance().setMenuTrack("assets/music/Every Block You Take.mp3");
|
||||
}).detach();
|
||||
menuTrackLoaded = true;
|
||||
}
|
||||
|
||||
// Start appropriate music based on state
|
||||
if (state == AppState::Menu) {
|
||||
Audio::instance().playMenuMusic();
|
||||
} else {
|
||||
Audio::instance().playGameMusic();
|
||||
}
|
||||
musicStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle music transitions between states
|
||||
static AppState previousState = AppState::Loading;
|
||||
if (state != previousState && musicStarted) {
|
||||
if (state == AppState::Menu && previousState == AppState::Playing) {
|
||||
// Switched from game to menu
|
||||
Audio::instance().playMenuMusic();
|
||||
} else if (state == AppState::Playing && previousState == AppState::Menu) {
|
||||
// Switched from menu to game
|
||||
Audio::instance().playGameMusic();
|
||||
}
|
||||
}
|
||||
previousState = state;
|
||||
|
||||
// Update starfields based on current state
|
||||
if (state == AppState::Loading) {
|
||||
starfield3D.update(float(frameMs / 1000.0f));
|
||||
@ -1242,409 +1313,141 @@ int main(int, char **)
|
||||
}
|
||||
break;
|
||||
case AppState::Playing:
|
||||
{
|
||||
// Calculate actual content area (centered within the window)
|
||||
float contentScale = logicalScale;
|
||||
float contentW = LOGICAL_W * contentScale;
|
||||
float contentH = LOGICAL_H * contentScale;
|
||||
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
|
||||
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
|
||||
|
||||
// Draw the game with layout matching the JavaScript version
|
||||
auto drawRect = [&](float x, float y, float w, float h, SDL_Color c)
|
||||
GameRenderer::renderPlayingState(
|
||||
renderer,
|
||||
&game,
|
||||
&pixelFont,
|
||||
&lineEffect,
|
||||
blocksTex,
|
||||
(float)LOGICAL_W,
|
||||
(float)LOGICAL_H,
|
||||
logicalScale,
|
||||
(float)winW,
|
||||
(float)winH,
|
||||
showExitConfirmPopup
|
||||
);
|
||||
break;
|
||||
case AppState::GameOver:
|
||||
// Draw the game state in the background
|
||||
GameRenderer::renderPlayingState(
|
||||
renderer,
|
||||
&game,
|
||||
&pixelFont,
|
||||
&lineEffect,
|
||||
blocksTex,
|
||||
(float)LOGICAL_W,
|
||||
(float)LOGICAL_H,
|
||||
logicalScale,
|
||||
(float)winW,
|
||||
(float)winH,
|
||||
false // No exit popup in Game Over
|
||||
);
|
||||
|
||||
// Draw Game Over Overlay
|
||||
{
|
||||
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
|
||||
SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h};
|
||||
SDL_RenderFillRect(renderer, &fr);
|
||||
};
|
||||
|
||||
// Responsive layout that scales with window size while maintaining margins
|
||||
// Calculate available space considering UI panels and margins
|
||||
const float MIN_MARGIN = 40.0f; // Minimum margin from edges
|
||||
const float TOP_MARGIN = 60.0f; // Extra top margin for better spacing
|
||||
const float PANEL_WIDTH = 180.0f; // Width of side panels
|
||||
const float PANEL_SPACING = 30.0f; // Space between grid and panels
|
||||
const float NEXT_PIECE_HEIGHT = 120.0f; // Space reserved for next piece preview (increased)
|
||||
const float BOTTOM_MARGIN = 60.0f; // Space for controls text at bottom
|
||||
|
||||
// Available width = Total width - margins - left panel - right panel - spacing
|
||||
const float availableWidth = LOGICAL_W - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2);
|
||||
const float availableHeight = LOGICAL_H - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PIECE_HEIGHT;
|
||||
|
||||
// Calculate block size based on available space (maintain 10:20 aspect ratio)
|
||||
const float maxBlockSizeW = availableWidth / Game::COLS;
|
||||
const float maxBlockSizeH = availableHeight / Game::ROWS;
|
||||
const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH);
|
||||
|
||||
// Ensure minimum and maximum block sizes
|
||||
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;
|
||||
|
||||
// Calculate vertical position with proper top margin
|
||||
const float totalContentHeight = NEXT_PIECE_HEIGHT + GRID_H;
|
||||
const float availableVerticalSpace = LOGICAL_H - TOP_MARGIN - BOTTOM_MARGIN;
|
||||
const float verticalCenterOffset = (availableVerticalSpace - totalContentHeight) * 0.5f;
|
||||
const float contentStartY = TOP_MARGIN + verticalCenterOffset;
|
||||
|
||||
// Perfect horizontal centering - center the entire layout (grid + panels) in the window
|
||||
const float totalLayoutWidth = PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + PANEL_WIDTH;
|
||||
const float layoutStartX = (LOGICAL_W - totalLayoutWidth) * 0.5f;
|
||||
|
||||
// Calculate panel and grid positions from the centered layout
|
||||
const float statsX = layoutStartX + contentOffsetX;
|
||||
const float gridX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + contentOffsetX;
|
||||
const float scoreX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + contentOffsetX;
|
||||
|
||||
// Position grid with proper top spacing
|
||||
const float gridY = contentStartY + NEXT_PIECE_HEIGHT + contentOffsetY;
|
||||
|
||||
// Panel dimensions and positions
|
||||
const float statsY = gridY;
|
||||
const float statsW = PANEL_WIDTH;
|
||||
const float statsH = GRID_H;
|
||||
|
||||
const float scoreY = gridY;
|
||||
const float scoreW = PANEL_WIDTH;
|
||||
|
||||
// Next piece preview (above grid, centered)
|
||||
const float nextW = finalBlockSize * 4 + 20;
|
||||
const float nextH = finalBlockSize * 2 + 20;
|
||||
const float nextX = gridX + (GRID_W - nextW) * 0.5f;
|
||||
const float nextY = contentStartY + contentOffsetY;
|
||||
|
||||
// Handle line clearing effects (now that we have grid coordinates)
|
||||
if (game.hasCompletedLines() && !lineEffect.isActive()) {
|
||||
auto completedLines = game.getCompletedLines();
|
||||
lineEffect.startLineClear(completedLines, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
||||
}
|
||||
|
||||
// Draw panels with borders (like JS version)
|
||||
|
||||
// Game grid border
|
||||
drawRect(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255}); // Outer border
|
||||
drawRect(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255}); // Inner border
|
||||
drawRect(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255}); // Background
|
||||
|
||||
// Left panel background (BLOCKS panel) - translucent, slightly shorter height
|
||||
{
|
||||
SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160);
|
||||
SDL_FRect lbg{statsX - 16, gridY - 10, statsW + 32, GRID_H + 20};
|
||||
SDL_RenderFillRect(renderer, &lbg);
|
||||
}
|
||||
// Right panel background (SCORE/LINES/LEVEL etc) - translucent
|
||||
{
|
||||
SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160);
|
||||
SDL_FRect rbg{scoreX - 16, gridY - 16, scoreW + 32, GRID_H + 32};
|
||||
SDL_RenderFillRect(renderer, &rbg);
|
||||
}
|
||||
|
||||
// Draw grid lines (subtle lines to show cell boundaries)
|
||||
SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255); // Slightly lighter than background
|
||||
|
||||
// Vertical grid lines
|
||||
for (int x = 1; x < Game::COLS; ++x) {
|
||||
float lineX = gridX + x * finalBlockSize; // Remove duplicate contentOffsetX
|
||||
SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H);
|
||||
}
|
||||
|
||||
// Horizontal grid lines
|
||||
for (int y = 1; y < Game::ROWS; ++y) {
|
||||
float lineY = gridY + y * finalBlockSize; // Remove duplicate contentOffsetY
|
||||
SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY);
|
||||
}
|
||||
|
||||
// Block statistics panel
|
||||
drawRect(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255});
|
||||
drawRect(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255});
|
||||
|
||||
// Next piece preview panel
|
||||
drawRect(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255});
|
||||
drawRect(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255});
|
||||
|
||||
// Draw the game board
|
||||
const auto &board = game.boardRef();
|
||||
for (int y = 0; y < Game::ROWS; ++y)
|
||||
{
|
||||
for (int x = 0; x < Game::COLS; ++x)
|
||||
{
|
||||
int v = board[y * Game::COLS + x];
|
||||
if (v > 0) {
|
||||
float bx = gridX + x * finalBlockSize;
|
||||
float by = gridY + y * finalBlockSize;
|
||||
drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw ghost piece (where current piece will land)
|
||||
if (!game.isPaused()) {
|
||||
Game::Piece ghostPiece = game.current();
|
||||
// Find landing position
|
||||
while (true) {
|
||||
Game::Piece testPiece = ghostPiece;
|
||||
testPiece.y++;
|
||||
bool collision = false;
|
||||
|
||||
// Simple collision check
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(testPiece, cx, cy)) {
|
||||
int gx = testPiece.x + cx;
|
||||
int gy = testPiece.y + cy;
|
||||
if (gy >= Game::ROWS || gx < 0 || gx >= Game::COLS ||
|
||||
(gy >= 0 && board[gy * Game::COLS + gx] != 0)) {
|
||||
collision = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (collision) break;
|
||||
}
|
||||
|
||||
if (collision) break;
|
||||
ghostPiece = testPiece;
|
||||
}
|
||||
|
||||
// Draw ghost piece
|
||||
drawPiece(renderer, blocksTex, ghostPiece, gridX, gridY, finalBlockSize, true);
|
||||
}
|
||||
|
||||
// Draw the falling piece
|
||||
if (!game.isPaused()) {
|
||||
drawPiece(renderer, blocksTex, game.current(), gridX, gridY, finalBlockSize, false);
|
||||
}
|
||||
|
||||
// Handle line clearing effects
|
||||
if (game.hasCompletedLines() && !lineEffect.isActive()) {
|
||||
lineEffect.startLineClear(game.getCompletedLines(), static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
||||
}
|
||||
|
||||
// Draw line clearing effects
|
||||
if (lineEffect.isActive()) {
|
||||
lineEffect.render(renderer, blocksTex, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
|
||||
}
|
||||
|
||||
// Draw next piece preview
|
||||
pixelFont.draw(renderer, nextX + 10, nextY - 20, "NEXT", 1.0f, {255, 220, 0, 255});
|
||||
if (game.next().type < PIECE_COUNT) {
|
||||
drawSmallPiece(renderer, blocksTex, game.next().type, nextX + 10, nextY + 10, finalBlockSize * 0.6f);
|
||||
}
|
||||
|
||||
// Draw block statistics (left panel)
|
||||
pixelFont.draw(renderer, statsX + 10, statsY + 10, "BLOCKS", 1.0f, {255, 220, 0, 255});
|
||||
|
||||
const auto& blockCounts = game.getBlockCounts();
|
||||
int totalBlocks = 0; for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i];
|
||||
const char* pieceNames[] = {"I", "O", "T", "S", "Z", "J", "L"};
|
||||
// Dynamic vertical cursor so bars sit below blocks cleanly
|
||||
float yCursor = statsY + 52;
|
||||
for (int i = 0; i < PIECE_COUNT; ++i) {
|
||||
// Baseline for this entry
|
||||
float py = yCursor;
|
||||
|
||||
// Draw small piece icon (top of entry)
|
||||
float previewSize = finalBlockSize * 0.55f;
|
||||
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(i), statsX + 18, py, previewSize);
|
||||
|
||||
// Compute preview height in tiles (rotation 0)
|
||||
int maxCy = -1;
|
||||
{
|
||||
Game::Piece prev; prev.type = static_cast<PieceType>(i); prev.rot = 0; prev.x = 0; prev.y = 0;
|
||||
for (int cy = 0; cy < 4; ++cy) {
|
||||
for (int cx = 0; cx < 4; ++cx) {
|
||||
if (Game::cellFilled(prev, cx, cy)) maxCy = std::max(maxCy, cy);
|
||||
}
|
||||
}
|
||||
}
|
||||
int tilesHigh = (maxCy >= 0 ? maxCy + 1 : 1);
|
||||
float previewHeight = tilesHigh * previewSize;
|
||||
|
||||
// Count on the right, near the top (aligned with blocks)
|
||||
int count = blockCounts[i];
|
||||
char countStr[16];
|
||||
snprintf(countStr, sizeof(countStr), "%d", count);
|
||||
pixelFont.draw(renderer, statsX + statsW - 20, py + 6, countStr, 1.1f, {240, 240, 245, 255});
|
||||
|
||||
// Percentage and bar BELOW the blocks
|
||||
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 = statsX + 12;
|
||||
float barY = py + previewHeight + 18.0f;
|
||||
float barW = statsW - 24;
|
||||
float barH = 6;
|
||||
|
||||
// Percent text just above the bar (left)
|
||||
pixelFont.draw(renderer, barX, barY - 16, percStr, 0.8f, {230, 230, 235, 255});
|
||||
|
||||
// Track
|
||||
SDL_SetRenderDrawColor(renderer, 170, 170, 175, 200);
|
||||
SDL_FRect track{barX, barY, barW, barH};
|
||||
SDL_RenderFillRect(renderer, &track);
|
||||
// Fill (piece color)
|
||||
SDL_Color pc = COLORS[i + 1];
|
||||
SDL_SetRenderDrawColor(renderer, pc.r, pc.g, pc.b, 230);
|
||||
float fillW = barW * (perc / 100.0f);
|
||||
if (fillW < 0) fillW = 0; if (fillW > barW) fillW = barW;
|
||||
SDL_FRect fill{barX, barY, fillW, barH};
|
||||
SDL_RenderFillRect(renderer, &fill);
|
||||
|
||||
// Advance cursor: bar bottom + spacing
|
||||
yCursor = barY + barH + 18.0f;
|
||||
}
|
||||
|
||||
// Draw score panel (right side), centered vertically in grid
|
||||
// Compute content vertical centering based on known offsets
|
||||
const float contentTopOffset = 0.0f;
|
||||
const float contentBottomOffset = 290.0f; // last line (time value)
|
||||
const float contentPad = 36.0f;
|
||||
float scoreContentH = (contentBottomOffset - contentTopOffset) + contentPad;
|
||||
float baseY = gridY + (GRID_H - scoreContentH) * 0.5f;
|
||||
|
||||
pixelFont.draw(renderer, scoreX, baseY + 0, "SCORE", 1.0f, {255, 220, 0, 255});
|
||||
char scoreStr[32];
|
||||
snprintf(scoreStr, sizeof(scoreStr), "%d", game.score());
|
||||
pixelFont.draw(renderer, scoreX, baseY + 25, scoreStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
pixelFont.draw(renderer, scoreX, baseY + 70, "LINES", 1.0f, {255, 220, 0, 255});
|
||||
char linesStr[16];
|
||||
snprintf(linesStr, sizeof(linesStr), "%03d", game.lines());
|
||||
pixelFont.draw(renderer, scoreX, baseY + 95, linesStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
pixelFont.draw(renderer, scoreX, baseY + 140, "LEVEL", 1.0f, {255, 220, 0, 255});
|
||||
char levelStr[16];
|
||||
snprintf(levelStr, sizeof(levelStr), "%02d", game.level());
|
||||
pixelFont.draw(renderer, scoreX, baseY + 165, levelStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
// Next level progress
|
||||
// JS rules: first threshold = (startLevel+1)*10; afterwards every +10
|
||||
int startLv = game.startLevelBase(); // 0-based
|
||||
int firstThreshold = (startLv + 1) * 10;
|
||||
int linesDone = game.lines();
|
||||
int nextThreshold = 0;
|
||||
if (linesDone < firstThreshold) {
|
||||
nextThreshold = firstThreshold;
|
||||
} else {
|
||||
int blocksPast = linesDone - firstThreshold;
|
||||
nextThreshold = firstThreshold + ((blocksPast / 10) + 1) * 10;
|
||||
}
|
||||
int linesForNext = std::max(0, nextThreshold - linesDone);
|
||||
pixelFont.draw(renderer, scoreX, baseY + 200, "NEXT LVL", 1.0f, {255, 220, 0, 255});
|
||||
char nextStr[32];
|
||||
snprintf(nextStr, sizeof(nextStr), "%d LINES", linesForNext);
|
||||
pixelFont.draw(renderer, scoreX, baseY + 225, nextStr, 0.9f, {80, 255, 120, 255});
|
||||
|
||||
// Time
|
||||
pixelFont.draw(renderer, scoreX, baseY + 265, "TIME", 1.0f, {255, 220, 0, 255});
|
||||
int totalSecs = static_cast<int>(game.elapsed());
|
||||
int mins = totalSecs / 60;
|
||||
int secs = totalSecs % 60;
|
||||
char timeStr[16];
|
||||
snprintf(timeStr, sizeof(timeStr), "%02d:%02d", mins, secs);
|
||||
pixelFont.draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255});
|
||||
|
||||
// --- Gravity HUD: show current gravity in ms and equivalent fps (top-right) ---
|
||||
{
|
||||
char gms[64];
|
||||
double gms_val = game.getGravityMs();
|
||||
double gfps = gms_val > 0.0 ? (1000.0 / gms_val) : 0.0;
|
||||
snprintf(gms, sizeof(gms), "GRAV: %.0f ms (%.2f fps)", gms_val, gfps);
|
||||
pixelFont.draw(renderer, LOGICAL_W - 260, 10, gms, 0.9f, {200, 200, 220, 255});
|
||||
}
|
||||
|
||||
// Hold piece (if implemented)
|
||||
if (game.held().type < PIECE_COUNT) {
|
||||
pixelFont.draw(renderer, statsX + 10, statsY + statsH - 80, "HOLD", 1.0f, {255, 220, 0, 255});
|
||||
drawSmallPiece(renderer, blocksTex, game.held().type, statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f);
|
||||
}
|
||||
|
||||
// Pause overlay: don't draw pause UI when the exit-confirm popup is showing
|
||||
if (game.isPaused() && !showExitConfirmPopup) {
|
||||
// Semi-transparent overlay
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
|
||||
SDL_FRect pauseOverlay{0, 0, LOGICAL_W, LOGICAL_H};
|
||||
SDL_RenderFillRect(renderer, &pauseOverlay);
|
||||
|
||||
// Pause text
|
||||
pixelFont.draw(renderer, LOGICAL_W * 0.5f - 80, LOGICAL_H * 0.5f - 20, "PAUSED", 2.0f, {255, 255, 255, 255});
|
||||
pixelFont.draw(renderer, LOGICAL_W * 0.5f - 120, LOGICAL_H * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255});
|
||||
}
|
||||
|
||||
// Exit confirmation popup (modal)
|
||||
if (showExitConfirmPopup) {
|
||||
// Compute content offsets for consistent placement across window sizes
|
||||
float contentW = LOGICAL_W * logicalScale;
|
||||
float contentH = LOGICAL_H * logicalScale;
|
||||
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
||||
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
||||
|
||||
float popupW = 420, popupH = 180;
|
||||
float popupX = (LOGICAL_W - popupW) / 2;
|
||||
float popupY = (LOGICAL_H - popupH) / 2;
|
||||
|
||||
// Dim entire window (use window coordinates so it always covers 100% of the target)
|
||||
SDL_SetRenderViewport(renderer, nullptr);
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200);
|
||||
// 1. Dim the background
|
||||
SDL_SetRenderViewport(renderer, nullptr); // Use window coordinates for full screen dim
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); // Dark semi-transparent
|
||||
SDL_FRect fullWin{0.f, 0.f, (float)winW, (float)winH};
|
||||
SDL_RenderFillRect(renderer, &fullWin);
|
||||
// Restore logical viewport for drawing content-local popup
|
||||
|
||||
// Restore logical viewport
|
||||
SDL_SetRenderViewport(renderer, &logicalVP);
|
||||
SDL_SetRenderScale(renderer, logicalScale, logicalScale);
|
||||
|
||||
// Draw popup box (drawRect will apply contentOffset internally)
|
||||
drawRect(popupX - 4, popupY - 4, popupW + 8, popupH + 8, {60, 70, 90, 255});
|
||||
drawRect(popupX, popupY, popupW, popupH, {20, 22, 28, 240});
|
||||
// 2. Calculate content offsets (same as in GameRenderer)
|
||||
float contentScale = logicalScale;
|
||||
float contentW = LOGICAL_W * contentScale;
|
||||
float contentH = LOGICAL_H * contentScale;
|
||||
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
|
||||
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
|
||||
|
||||
// Center title and body text inside popup (use pixelFont for retro P2 font)
|
||||
const std::string title = "Exit game?";
|
||||
const std::string line1 = "Are you sure you want to";
|
||||
const std::string line2 = "leave the current game?";
|
||||
// 3. Draw Game Over Box
|
||||
float boxW = 500.0f;
|
||||
float boxH = 350.0f;
|
||||
float boxX = (LOGICAL_W - boxW) * 0.5f;
|
||||
float boxY = (LOGICAL_H - boxH) * 0.5f;
|
||||
|
||||
int wTitle=0,hTitle=0; pixelFont.measure(title, 1.6f, wTitle, hTitle);
|
||||
int wL1=0,hL1=0; pixelFont.measure(line1, 0.9f, wL1, hL1);
|
||||
int wL2=0,hL2=0; pixelFont.measure(line2, 0.9f, wL2, hL2);
|
||||
// Draw box background
|
||||
SDL_SetRenderDrawColor(renderer, 20, 25, 35, 255);
|
||||
SDL_FRect boxRect{boxX + contentOffsetX, boxY + contentOffsetY, boxW, boxH};
|
||||
SDL_RenderFillRect(renderer, &boxRect);
|
||||
|
||||
// Draw box border
|
||||
SDL_SetRenderDrawColor(renderer, 100, 120, 200, 255);
|
||||
SDL_FRect borderRect{boxX + contentOffsetX - 3, boxY + contentOffsetY - 3, boxW + 6, boxH + 6};
|
||||
SDL_RenderFillRect(renderer, &borderRect); // Use FillRect for border background effect
|
||||
SDL_SetRenderDrawColor(renderer, 20, 25, 35, 255);
|
||||
SDL_RenderFillRect(renderer, &boxRect); // Redraw background on top of border rect
|
||||
|
||||
float titleX = popupX + (popupW - (float)wTitle) * 0.5f;
|
||||
float l1X = popupX + (popupW - (float)wL1) * 0.5f;
|
||||
float l2X = popupX + (popupW - (float)wL2) * 0.5f;
|
||||
// 4. Draw Text
|
||||
// 4. Draw Text
|
||||
// Title
|
||||
bool realHighScore = scores.isHighScore(game.score());
|
||||
const char* title = realHighScore ? "NEW HIGH SCORE!" : "GAME OVER";
|
||||
int tW=0, tH=0; pixelFont.measure(title, 2.0f, tW, tH);
|
||||
pixelFont.draw(renderer, boxX + (boxW - tW) * 0.5f + contentOffsetX, boxY + 40 + contentOffsetY, title, 2.0f, realHighScore ? SDL_Color{255, 220, 0, 255} : SDL_Color{255, 60, 60, 255});
|
||||
|
||||
pixelFont.draw(renderer, titleX + contentOffsetX, popupY + contentOffsetY + 20, title, 1.6f, {255, 220, 0, 255});
|
||||
pixelFont.draw(renderer, l1X + contentOffsetX, popupY + contentOffsetY + 60, line1, 0.9f, SDL_Color{220,220,230,255});
|
||||
pixelFont.draw(renderer, l2X + contentOffsetX, popupY + contentOffsetY + 84, line2, 0.9f, SDL_Color{220,220,230,255});
|
||||
// Score
|
||||
char scoreStr[64];
|
||||
snprintf(scoreStr, sizeof(scoreStr), "SCORE: %d", game.score());
|
||||
int sW=0, sH=0; pixelFont.measure(scoreStr, 1.2f, sW, sH);
|
||||
pixelFont.draw(renderer, boxX + (boxW - sW) * 0.5f + contentOffsetX, boxY + 100 + contentOffsetY, scoreStr, 1.2f, {255, 255, 255, 255});
|
||||
|
||||
// Buttons (center labels inside buttons) - use pixelFont for labels
|
||||
float btnW = 140, btnH = 46;
|
||||
float yesX = popupX + popupW * 0.25f - btnW/2.0f;
|
||||
float noX = popupX + popupW * 0.75f - btnW/2.0f;
|
||||
float btnY = popupY + popupH - 60;
|
||||
if (isNewHighScore) {
|
||||
// Name Entry
|
||||
const char* enterName = "ENTER NAME:";
|
||||
int enW=0, enH=0; pixelFont.measure(enterName, 1.0f, enW, enH);
|
||||
pixelFont.draw(renderer, boxX + (boxW - enW) * 0.5f + contentOffsetX, boxY + 160 + contentOffsetY, enterName, 1.0f, {200, 200, 220, 255});
|
||||
|
||||
drawRect(yesX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255});
|
||||
drawRect(yesX, btnY, btnW, btnH, {200, 60, 60, 255});
|
||||
const std::string yes = "YES";
|
||||
int wy=0,hy=0; pixelFont.measure(yes, 1.0f, wy, hy);
|
||||
pixelFont.draw(renderer, yesX + (btnW - (float)wy) * 0.5f + contentOffsetX, btnY + (btnH - (float)hy) * 0.5f + contentOffsetY, yes, 1.0f, {255,255,255,255});
|
||||
// Input box
|
||||
float inputW = 300.0f;
|
||||
float inputH = 40.0f;
|
||||
float inputX = boxX + (boxW - inputW) * 0.5f;
|
||||
float inputY = boxY + 200.0f;
|
||||
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
||||
SDL_FRect inputRect{inputX + contentOffsetX, inputY + contentOffsetY, inputW, inputH};
|
||||
SDL_RenderFillRect(renderer, &inputRect);
|
||||
|
||||
SDL_SetRenderDrawColor(renderer, 255, 220, 0, 255);
|
||||
SDL_RenderRect(renderer, &inputRect);
|
||||
|
||||
drawRect(noX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255});
|
||||
drawRect(noX, btnY, btnW, btnH, {80, 140, 80, 255});
|
||||
const std::string no = "NO";
|
||||
int wn=0,hn=0; pixelFont.measure(no, 1.0f, wn, hn);
|
||||
pixelFont.draw(renderer, noX + (btnW - (float)wn) * 0.5f + contentOffsetX, btnY + (btnH - (float)hn) * 0.5f + contentOffsetY, no, 1.0f, {255,255,255,255});
|
||||
// Player Name (with cursor)
|
||||
std::string display = playerName;
|
||||
if ((SDL_GetTicks() / 500) % 2 == 0) display += "_"; // Blink cursor
|
||||
|
||||
int nW=0, nH=0; pixelFont.measure(display, 1.2f, nW, nH);
|
||||
pixelFont.draw(renderer, inputX + (inputW - nW) * 0.5f + contentOffsetX, inputY + (inputH - nH) * 0.5f + contentOffsetY, display, 1.2f, {255, 255, 255, 255});
|
||||
|
||||
// Hint
|
||||
const char* hint = "PRESS ENTER TO SUBMIT";
|
||||
int hW=0, hH=0; pixelFont.measure(hint, 0.8f, hW, hH);
|
||||
pixelFont.draw(renderer, boxX + (boxW - hW) * 0.5f + contentOffsetX, boxY + 280 + contentOffsetY, hint, 0.8f, {150, 150, 150, 255});
|
||||
|
||||
} else {
|
||||
// Lines
|
||||
char linesStr[64];
|
||||
snprintf(linesStr, sizeof(linesStr), "LINES: %d", game.lines());
|
||||
int lW=0, lH=0; pixelFont.measure(linesStr, 1.2f, lW, lH);
|
||||
pixelFont.draw(renderer, boxX + (boxW - lW) * 0.5f + contentOffsetX, boxY + 140 + contentOffsetY, linesStr, 1.2f, {255, 255, 255, 255});
|
||||
|
||||
// Level
|
||||
char levelStr[64];
|
||||
snprintf(levelStr, sizeof(levelStr), "LEVEL: %d", game.level());
|
||||
int lvW=0, lvH=0; pixelFont.measure(levelStr, 1.2f, lvW, lvH);
|
||||
pixelFont.draw(renderer, boxX + (boxW - lvW) * 0.5f + contentOffsetX, boxY + 180 + contentOffsetY, levelStr, 1.2f, {255, 255, 255, 255});
|
||||
|
||||
// Instructions
|
||||
const char* instr = "PRESS ENTER TO RESTART";
|
||||
int iW=0, iH=0; pixelFont.measure(instr, 0.9f, iW, iH);
|
||||
pixelFont.draw(renderer, boxX + (boxW - iW) * 0.5f + contentOffsetX, boxY + 260 + contentOffsetY, instr, 0.9f, {255, 220, 0, 255});
|
||||
|
||||
const char* instr2 = "PRESS ESC FOR MENU";
|
||||
int iW2=0, iH2=0; pixelFont.measure(instr2, 0.9f, iW2, iH2);
|
||||
pixelFont.draw(renderer, boxX + (boxW - iW2) * 0.5f + contentOffsetX, boxY + 290 + contentOffsetY, instr2, 0.9f, {255, 220, 0, 255});
|
||||
}
|
||||
}
|
||||
|
||||
// Controls hint at bottom
|
||||
font.draw(renderer, 20, LOGICAL_H - 30, "ARROWS=Move Z/X=Rotate C=Hold SPACE=Drop P=Pause ESC=Menu", 1.0f, {150, 150, 170, 255});
|
||||
}
|
||||
break;
|
||||
case AppState::GameOver:
|
||||
font.draw(renderer, LOGICAL_W * 0.5f - 120, 140, "GAME OVER", 3.0f, SDL_Color{255, 80, 60, 255});
|
||||
{
|
||||
char buf[128];
|
||||
std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d", game.score(), game.lines(), game.level());
|
||||
font.draw(renderer, LOGICAL_W * 0.5f - 120, 220, buf, 1.2f, SDL_Color{220, 220, 230, 255});
|
||||
}
|
||||
font.draw(renderer, LOGICAL_W * 0.5f - 120, 270, "PRESS ENTER / SPACE", 1.2f, SDL_Color{200, 200, 220, 255});
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user