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:
@ -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() {
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user