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:
2025-11-22 09:47:46 +01:00
parent 66099809e0
commit ec2bb1bb1e
20 changed files with 387 additions and 2316 deletions

View File

@ -1,9 +1,19 @@
// Scores.cpp - Implementation of ScoreManager (copied into src/persistence)
// Scores.cpp - Implementation of ScoreManager with Firebase Sync
#include "Scores.h"
#include <SDL3/SDL.h>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cpr/cpr.h>
#include <nlohmann/json.hpp>
#include <iostream>
#include <thread>
#include <ctime>
using json = nlohmann::json;
// Firebase Realtime Database URL
const std::string FIREBASE_URL = "https://tetris-90139.firebaseio.com/scores.json";
ScoreManager::ScoreManager(size_t maxScores) : maxEntries(maxScores) {}
@ -16,6 +26,52 @@ std::string ScoreManager::filePath() const {
void ScoreManager::load() {
scores.clear();
// Try to load from Firebase first
try {
cpr::Response r = cpr::Get(cpr::Url{FIREBASE_URL}, cpr::Timeout{2000}); // 2s timeout
if (r.status_code == 200 && !r.text.empty() && r.text != "null") {
auto j = json::parse(r.text);
// Firebase returns a map of auto-generated IDs to objects
if (j.is_object()) {
for (auto& [key, value] : j.items()) {
ScoreEntry e;
if (value.contains("score")) e.score = value["score"];
if (value.contains("lines")) e.lines = value["lines"];
if (value.contains("level")) e.level = value["level"];
if (value.contains("timeSec")) e.timeSec = value["timeSec"];
if (value.contains("name")) e.name = value["name"];
scores.push_back(e);
}
}
// Or it might be an array if keys are integers (unlikely for Firebase push)
else if (j.is_array()) {
for (auto& value : j) {
ScoreEntry e;
if (value.contains("score")) e.score = value["score"];
if (value.contains("lines")) e.lines = value["lines"];
if (value.contains("level")) e.level = value["level"];
if (value.contains("timeSec")) e.timeSec = value["timeSec"];
if (value.contains("name")) e.name = value["name"];
scores.push_back(e);
}
}
// Sort and keep top scores
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
if (scores.size() > maxEntries) scores.resize(maxEntries);
// Save to local cache
save();
return;
}
} catch (...) {
// Ignore network errors and fall back to local file
std::cerr << "Failed to load from Firebase, falling back to local file." << std::endl;
}
// Fallback to local file
std::ifstream f(filePath());
if (!f) {
// Create sample high scores if file doesn't exist
@ -56,11 +112,43 @@ void ScoreManager::save() const {
}
}
void ScoreManager::submit(int score, int lines, int level, double timeSec) {
scores.push_back(ScoreEntry{score,lines,level,timeSec});
void ScoreManager::submit(int score, int lines, int level, double timeSec, const std::string& name) {
// Add to local list
scores.push_back(ScoreEntry{score,lines,level,timeSec, name});
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
if (scores.size()>maxEntries) scores.resize(maxEntries);
save();
// Submit to Firebase
// Run in a detached thread to avoid blocking the UI?
// For simplicity, we'll do it blocking for now, or rely on short timeout.
// Ideally this should be async.
json j;
j["score"] = score;
j["lines"] = lines;
j["level"] = level;
j["timeSec"] = timeSec;
j["name"] = name;
j["timestamp"] = std::time(nullptr); // Add timestamp
// Fire and forget (async) would be better, but for now let's just try to send
// We can use std::thread to make it async
std::thread([j]() {
try {
cpr::Post(cpr::Url{FIREBASE_URL},
cpr::Body{j.dump()},
cpr::Header{{"Content-Type", "application/json"}},
cpr::Timeout{5000});
} catch (...) {
// Ignore errors
}
}).detach();
}
bool ScoreManager::isHighScore(int score) const {
if (scores.size() < maxEntries) return true;
return score > scores.back().score;
}
void ScoreManager::createSampleScores() {

View File

@ -10,7 +10,8 @@ public:
explicit ScoreManager(size_t maxScores = 12);
void load();
void save() const;
void submit(int score, int lines, int level, double timeSec);
void submit(int score, int lines, int level, double timeSec, const std::string& name = "PLAYER");
bool isHighScore(int score) const;
const std::vector<ScoreEntry>& all() const { return scores; }
private:
std::vector<ScoreEntry> scores;