feat(audio): musica acelerada en CONTINUE?, game_over.ogg, tick del countdown

This commit is contained in:
JailDesigner committed 2026-05-08 22:06:44 +02:00
1 parent 5e862ceadc
commit a717411edd
13 files changed
+186 -2

No files matched your search

+7
View File
@@ -113,6 +113,13 @@ void Audio::stopMusic() {
}
}
// NOLINTNEXTLINE(readability-make-member-function-const)
void Audio::setMusicSpeed(float ratio) {
if (music_enabled_) {
engine_->setMusicSpeed(ratio);
}
}
// Reprodueix un so per nom
// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const
void Audio::playSound(const std::string& name, Group group) {
+5
View File
@@ -76,6 +76,11 @@ class Audio {
void stopMusic(); // Aturar completament la música
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
void setOnMusicEnded(std::function<void()> callback); // Callback disparat quan la pista actual acaba de drenar (CONV-03)
// Multiplicador de velocitat de la música actual. 1.0 = normal,
// 1.5 = un 50% més ràpid (efecte "chipmunk" — també puja el to).
// Es reseteja a 1.0 implícitament a cada `playMusic`. No-op si no
// hi ha música activa.
void setMusicSpeed(float ratio);
// --- Control de sons ---
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
+5
View File
@@ -286,6 +286,11 @@ namespace Ja {
}
}
void Engine::setMusicSpeed(float ratio) {
if (current_music_ == nullptr || current_music_->stream == nullptr) { return; }
SDL_SetAudioStreamFrequencyRatio(current_music_->stream, ratio);
}
void Engine::pauseMusic() {
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
+8
View File
@@ -171,6 +171,14 @@ namespace Ja {
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
[[nodiscard]] auto getMusicState() const -> MusicState;
auto setMusicVolume(float volume) -> float;
// Multiplicador de velocitat de reproducció de la música actual
// via `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal, 2.0 =
// doble velocitat. Cal saber que també puja el to (efecte
// "chipmunk") — és el comportament arcade clàssic dels comptes
// enrere. Cada `playMusic` crea un stream nou amb ratio 1.0,
// així que un canvi de track reseteja la velocitat
// implícitament. No-op si no hi ha música activa.
void setMusicSpeed(float ratio);
// Registra un callback que es disparà quan la música actual acabi de
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
// stopMusic, així que el callback pot invocar playMusic sense córrer.
+3
View File
@@ -168,6 +168,9 @@ Director::Director() {
Resource::Cache::get().loadMusic("room_intro.ogg", Defaults::Paths::MUSIC_ROOM_INTRO);
Resource::Cache::get().loadMusic("playing.ogg", Defaults::Paths::MUSIC_PLAYING);
Resource::Cache::get().loadMusic("room_outro.ogg", Defaults::Paths::MUSIC_ROOM_OUTRO);
// Pista de game over: pre-load igual que la resta perquè el playMusic
// del moment en què es dispara la sub-FSM no facti disc.
Resource::Cache::get().loadMusic("game_over.ogg", Defaults::Paths::MUSIC_GAME_OVER);
// Música de la TITLE: arrenca al constructor de l'escena i es fa
// fade-out quan l'escena entra a STARTING.
Resource::Cache::get().loadMusic("title.ogg", Defaults::Paths::MUSIC_TITLE);
+9
View File
@@ -42,6 +42,15 @@ namespace GameOverFsm {
cfg.game_over_wait_ms = readChildInt(n, "game_over_wait_ms", cfg.game_over_wait_ms);
cfg.fade_out_ms = readChildInt(n, "fade_out_ms", cfg.fade_out_ms);
cfg.black_hold_ms = readChildInt(n, "black_hold_ms", cfg.black_hold_ms);
if (n.contains("continue_music_speed")) {
cfg.continue_music_speed = n["continue_music_speed"].get_value<float>();
}
if (n.contains("game_over_music_track")) {
cfg.game_over_music_track = n["game_over_music_track"].get_value<std::string>();
}
if (n.contains("countdown_sound_key")) {
cfg.countdown_sound_key = n["countdown_sound_key"].get_value<std::string>();
}
}
Log::info("GameOverFsm", "timings carregats des de {} (continue hold {}ms, fade {}ms)", yaml_path, cfg.continue_text_hold_ms, cfg.fade_out_ms);
+19
View File
@@ -73,6 +73,25 @@ namespace GameOverFsm {
// canvas negre opac abans que el GameScene demani la transició.
int fade_out_ms{500};
int black_hold_ms{500};
// Multiplicador de velocitat de la música mentre dura el
// CONTINUE_PROMPT. 1.0 = sense canvi, >1 = més ràpid (i amb el
// to més agut: és l'efecte arcade clàssic dels comptes enrere).
// Es restableix a 1.0 al sortir del prompt (revive o GAME_OVER).
float continue_music_speed{1.5F};
// Pista de música que sona durant l'estat GAME_OVER (sense loop).
// Si la pista no està carregada al Resource::Cache, s'omet la
// reproducció i la fase manté els timings configurats. La durada
// de la pista determina implícitament `game_over_text_hold_ms`
// (vegeu `GameScene::setGameOverState`).
std::string game_over_music_track{"game_over.ogg"};
// So que sona cada vegada que apareix un nou número del
// countdown (al primer cop que es mostra el `start`, cada
// canvi de número, i quan apareix el 0). NO sona en la
// transició posterior cap a GAME_OVER. Buit = sense so.
std::string countdown_sound_key{"countdown.wav"};
};
// Llegeix la configuració des del YAML. Si el fitxer manca o falta
+103 -2
View File
@@ -506,6 +506,8 @@ GameScene::GameScene(Config config)
// pròpia (`game_over:`).
game_over_config_ = GameOverFsm::loadConfig(Defaults::GameScene::GAME_YAML);
preloadGameOverAssets();
// Cinemàtica d'entrada de la primera sala: arrenca la pista
// `room_intro.ogg` i arma el callback de fi que farà spawn + salt
// a PLAYING. Cal cridar `enterRoomIntro` ací (i no només des
@@ -514,7 +516,18 @@ GameScene::GameScene(Config config)
enterRoomIntro();
}
GameScene::~GameScene() = default;
GameScene::~GameScene() {
// Aturem qualsevol música que estigués sonant al sortir de la
// GameScene. Això cobreix tres camins: (a) transició natural a
// LOGO al final del game over (l'única audio activa pot ser la
// pista `game_over.ogg`, que en aquell moment ja s'ha drenat);
// (b) tancament directe via service menu o ALT+F4; (c) qualsevol
// futura sortida no-natural. La LogoScene té la seva pròpia
// música i no s'ha de mesclar amb la del joc.
if (auto* audio = Audio::get(); audio != nullptr) {
audio->stopMusic();
}
}
auto GameScene::computeSpawn() -> PlayerSpawn {
// Centre de la columna del passadís en píxels canvas. La caixa de
@@ -711,6 +724,19 @@ void GameScene::onPlayerDisabled(int player_id) {
players_active_[player_id] = false;
}
void GameScene::preloadGameOverAssets() const {
// Pre-load del so del countdown: el dispara `updateGameOverFlow`
// cada vegada que canvia el número visible de CONTINUE? (incloent
// l'aparició del 0 — però NO la transició a GAME_OVER). Sense
// pre-load, la primera reproducció faria disc amb el countdown ja
// en marxa.
const auto& key = game_over_config_.countdown_sound_key;
if (key.empty()) { return; }
Resource::Cache::get().loadSound(
key,
std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + key);
}
void GameScene::handleJoinInput() {
// El "join in progress" es processa durant PLAYING (regla 1) i,
// com a cas especial, durant CONTINUE_PROMPT — ahí un START reviu
@@ -1112,9 +1138,62 @@ void GameScene::drawRoomTextLine(const std::string& text, int x, int y, std::uin
void GameScene::setGameOverState(GameOverFsm::State new_state) {
if (new_state == game_over_state_) { return; }
Log::info("GAMEOVER", "{} -> {}", GameOverFsm::stateName(game_over_state_), GameOverFsm::stateName(new_state));
const auto PREV = game_over_state_;
Log::info("GAMEOVER", "{} -> {}", GameOverFsm::stateName(PREV), GameOverFsm::stateName(new_state));
game_over_state_ = new_state;
game_over_transition_ = {};
auto* audio = Audio::get();
// Sortida de CONTINUE_PROMPT cap a OFF (revive): reinici de la
// música de joc des del principi a velocitat normal. Reaprofitem
// el track gravat a `config_.music_track` (sempre `playing.ogg`
// amb loop infinit). Si la sortida és cap a GAME_OVER, la branca
// de més avall ja talla la música i posa `game_over.ogg` — aquí
// saltem perquè el stop+play seria redundant.
if (PREV == GameOverFsm::State::CONTINUE_PROMPT && new_state == GameOverFsm::State::OFF && audio != nullptr) {
audio->stopMusic();
audio->playMusic(config_.music_track, -1);
audio->setMusicSpeed(1.0F);
}
if (new_state == GameOverFsm::State::CONTINUE_PROMPT && audio != nullptr) {
// Acceleració arcade: stop+play perquè la pista arrenqui des
// del principi al ritme accelerat (sense aquest reinici, el
// canvi de velocitat sobre la posició actual queda poc
// perceptible amb ratios subtils com 1.05). El to també puja:
// és l'efecte clàssic dels comptes enrere.
audio->stopMusic();
audio->playMusic(config_.music_track, -1);
audio->setMusicSpeed(game_over_config_.continue_music_speed);
}
// Reset del tracker del so del countdown cada vegada que entrem
// a CONTINUE_PROMPT, perquè la primera aparició del valor inicial
// (`continue_count_start`) dispari el "tick". Fora de
// CONTINUE_PROMPT no s'usa.
if (new_state == GameOverFsm::State::CONTINUE_PROMPT) {
last_countdown_value_ = -1;
}
if (new_state == GameOverFsm::State::GAME_OVER && audio != nullptr) {
// Talla la música actual i arrenca la pista de GAME OVER sense
// loop. La fase de hold es redimensiona a la durada real de la
// pista perquè el text es mantingui visible mentre dura la
// música; l'exit anim, el wait i el fade arrenquen quan la
// música ja s'ha drenat.
audio->stopMusic();
const auto& track = game_over_config_.game_over_music_track;
if (!track.empty()) {
audio->playMusic(track, 0);
const int DUR_MS = Audio::getMusicDurationMs(track);
if (DUR_MS > 0) {
game_over_config_.game_over_text_hold_ms = DUR_MS;
Log::info("GAMEOVER", "{}: durada {} ms (text_hold sincronitzat)", track, DUR_MS);
}
}
}
if (new_state == GameOverFsm::State::FADE_OUT) {
// Arma el fade en el moment que entrem a la fase de fade-out.
// Mateix patró que `outro_fade_started_` per a OUTRO de sala.
@@ -1151,6 +1230,28 @@ void GameScene::updateGameOverFlow(int delta_ms) {
Log::info("GAMEOVER", "DONE -> requesting LOGO scene");
}
}
// Tick del so del countdown: cada vegada que el número visible
// canvia (incloent l'aparició del primer valor i del 0), reproduïm
// el so configurat. La detecció es fa post-`advance` perquè el
// canvi de fase 0 → 1 arribi a aparèixer com a transició -1 → start.
// En la transició cap a GAME_OVER el state ja no és CONTINUE_PROMPT
// i la branca s'omet — així no sona un "tick" extra al saltar.
if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) {
const int CURRENT = GameOverFsm::countdownSecondsRemaining(
game_over_state_,
game_over_transition_,
game_over_config_);
if (game_over_transition_.phase == 1 && CURRENT != last_countdown_value_) {
last_countdown_value_ = CURRENT;
const auto& key = game_over_config_.countdown_sound_key;
if (!key.empty()) {
if (auto* audio = Audio::get(); audio != nullptr) {
audio->playSound(key, Audio::Group::INTERFACE);
}
}
}
}
}
void GameScene::renderGameOverText() {
+12
View File
@@ -140,6 +140,12 @@ class GameScene : public Scene {
// mètode per acotar la complexitat cognitiva del subscriber/ctor.
void onPlayerDisabled(int player_id);
// Pre-load dels recursos d'àudio que s'usen mid-partida i no els
// toca el Director (so del countdown, etc.). Si la clau és buida
// o ja està al cache, és no-op. Extret a mètode per acotar la
// complexitat cognitiva del ctor.
void preloadGameOverAssets() const;
// Itera els slots actius i, si el delay configurat ha expirat des
// del callback de fi de música d'INTRO, dispara `triggerPlayerSpawn`.
// Cridada cada frame durant la fase d'spawn escalonat (entre el
@@ -307,6 +313,12 @@ class GameScene : public Scene {
Transitions::FadeToBlack game_over_fade_;
bool game_over_fade_started_{false};
// Últim valor del countdown reproduït amb so. -1 com a sentinel
// perquè la primera detecció del valor inicial (`continue_count_start`)
// dispari el "tick". Es reseteja a -1 cada vegada que entrem a
// CONTINUE_PROMPT (vegeu `setGameOverState`).
int last_countdown_value_{-1};
// Avança la sub-FSM de game over consumint `delta_ms`. Detecta la
// transició de "tots els jugadors fora de partida" durant PLAYING
// (entrada a CONTINUE_PROMPT), arma el fade de FADE_OUT i, en
+1
View File
@@ -508,6 +508,7 @@ namespace Defaults {
constexpr const char* MUSIC_PLAYING = "data/audio/music/playing.ogg";
constexpr const char* MUSIC_ROOM_INTRO = "data/audio/music/room_intro.ogg";
constexpr const char* MUSIC_ROOM_OUTRO = "data/audio/music/room_outro.ogg";
constexpr const char* MUSIC_GAME_OVER = "data/audio/music/game_over.ogg";
// Gràfics de logo / title pre-carregats pel Director.
constexpr const char* LOGO_MINI_GIF = "data/scenes/logo/logo_jailgames_mini.gif";