feat(audio): musica acelerada en CONTINUE?, game_over.ogg, tick del countdown
This commit is contained in:
1 parent
5e862ceadc
commit
a717411edd
13 files changed
+186
-2
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user