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

@ -104,25 +104,52 @@ void Audio::feed(Uint32 bytesWanted, SDL_AudioStream* stream){
std::vector<int16_t> mix(outSamples, 0);
// 1) Mix music into buffer (if not muted)
if(!muted && current >= 0){
// 1) Mix music into buffer (if not muted)
if(!muted && playing){
size_t cursorBytes = 0;
while(cursorBytes < bytesWanted){
if(current < 0) break;
auto &trk = tracks[current];
size_t samplesAvail = trk.pcm.size() - trk.cursor; // samples (int16)
if(samplesAvail == 0){ nextTrack(); if(current < 0) break; continue; }
AudioTrack* trk = nullptr;
if (isMenuMusic) {
if (menuTrack.ok) trk = &menuTrack;
} else {
if (current >= 0 && current < (int)tracks.size()) trk = &tracks[current];
}
if (!trk) break;
size_t samplesAvail = trk->pcm.size() - trk->cursor; // samples (int16)
if(samplesAvail == 0){
if (isMenuMusic) {
trk->cursor = 0; // Loop menu music
continue;
} else {
nextTrack();
if(current < 0) break;
continue;
}
}
size_t samplesNeeded = (bytesWanted - cursorBytes) / sizeof(int16_t);
size_t toCopy = (samplesAvail < samplesNeeded) ? samplesAvail : samplesNeeded;
if(toCopy == 0) break;
// Mix add with clamp
size_t startSample = cursorBytes / sizeof(int16_t);
for(size_t i=0;i<toCopy;++i){
int v = (int)mix[startSample+i] + (int)trk.pcm[trk.cursor+i];
int v = (int)mix[startSample+i] + (int)trk->pcm[trk->cursor+i];
if(v>32767) v=32767; if(v<-32768) v=-32768; mix[startSample+i] = (int16_t)v;
}
trk.cursor += toCopy;
trk->cursor += toCopy;
cursorBytes += (Uint32)(toCopy * sizeof(int16_t));
if(trk.cursor >= trk.pcm.size()) nextTrack();
if(trk->cursor >= trk->pcm.size()) {
if (isMenuMusic) {
trk->cursor = 0; // Loop menu music
} else {
nextTrack();
}
}
}
}
@ -266,6 +293,39 @@ int Audio::getLoadedTrackCount() const {
return loadedCount;
}
void Audio::setMenuTrack(const std::string& path) {
menuTrack.path = path;
#ifdef _WIN32
// Ensure MF is started (might be redundant if init called, but safe)
if(!mfStarted){ if(FAILED(MFStartup(MF_VERSION))) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MFStartup failed"); } else mfStarted=true; }
if (decodeMP3(path, menuTrack.pcm, menuTrack.rate, menuTrack.channels)) {
menuTrack.ok = true;
} else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode menu track %s", path.c_str());
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported (stub): %s", path.c_str());
#endif
}
void Audio::playMenuMusic() {
isMenuMusic = true;
if (menuTrack.ok) {
menuTrack.cursor = 0;
}
start();
}
void Audio::playGameMusic() {
isMenuMusic = false;
// If we were playing menu music, we might want to pick a random track or resume
if (current < 0 && !tracks.empty()) {
nextTrack();
}
start();
}
void Audio::shutdown(){
// Stop background loading thread first
if (loadingThread.joinable()) {