Merge branch 'chore/audio-reorg' — cicle de música per estat (INTRO/PLAYING/OUTRO) i sons de làtigo/HURT
This commit is contained in:
commit
a351f920ba
17 files changed
+233
-128
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -157,6 +157,7 @@ jump:
|
|||||||
hurt:
|
hurt:
|
||||||
speed: 120
|
speed: 120
|
||||||
brake_time_ms: 500
|
brake_time_ms: 500
|
||||||
|
sound: hurt.wav
|
||||||
|
|
||||||
# Configuració del látigo. Mateixos valors que Sam de moment;
|
# Configuració del látigo. Mateixos valors que Sam de moment;
|
||||||
# diferenciar quan equilibrem cada personatge.
|
# diferenciar quan equilibrem cada personatge.
|
||||||
@@ -165,3 +166,4 @@ whip:
|
|||||||
swing_ms: 200
|
swing_ms: 200
|
||||||
cooldown_ms: 200
|
cooldown_ms: 200
|
||||||
damage: 1
|
damage: 1
|
||||||
|
sound: whip.wav
|
||||||
@@ -157,6 +157,7 @@ jump:
|
|||||||
hurt:
|
hurt:
|
||||||
speed: 120
|
speed: 120
|
||||||
brake_time_ms: 500
|
brake_time_ms: 500
|
||||||
|
sound: hurt.wav
|
||||||
|
|
||||||
# Configuració del látigo. `range_tiles` és la longitud de la hitbox
|
# Configuració del látigo. `range_tiles` és la longitud de la hitbox
|
||||||
# abans de retallar contra murs/sarcs (3 tiles ≈ 48 px). `swing_ms` és
|
# abans de retallar contra murs/sarcs (3 tiles ≈ 48 px). `swing_ms` és
|
||||||
@@ -170,3 +171,4 @@ whip:
|
|||||||
swing_ms: 200
|
swing_ms: 200
|
||||||
cooldown_ms: 200
|
cooldown_ms: 200
|
||||||
damage: 1
|
damage: 1
|
||||||
|
sound: whip.wav
|
||||||
@@ -11,18 +11,17 @@ room_intro:
|
|||||||
text_hold_ms: 2000 # text quiet al centre
|
text_hold_ms: 2000 # text quiet al centre
|
||||||
text_exit_ms: 400 # text centre → fora per l'esquerra
|
text_exit_ms: 400 # text centre → fora per l'esquerra
|
||||||
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
|
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
|
||||||
player_spawn_delay_ms: 2400 # ms des de l'inici d'INTRO al spawn aeri.
|
# Delays d'spawn aeri **mesurats des de la fi de la música d'INTRO**
|
||||||
player2_spawn_delay_ms: 2700 # només mode BOTH. Trenca la simultaneïtat.
|
# (callback `Audio::setOnMusicEnded`). 0 = cau al moment exacte que
|
||||||
|
# `room_intro.ogg` acaba; valors > 0 escalonen els jugadors. En mode
|
||||||
|
# single-player s'usa només `player_spawn_delay_ms`.
|
||||||
|
player_spawn_delay_ms: 0
|
||||||
|
player2_spawn_delay_ms: 300
|
||||||
# Preset de reverb (catàleg a `data/sounds.yaml`) aplicat al primer
|
# Preset de reverb (catàleg a `data/sounds.yaml`) aplicat al primer
|
||||||
# LAND de cada jugador durant la cinemàtica d'entrada. La sala dona
|
# LAND de cada jugador durant la cinemàtica d'entrada. La sala dona
|
||||||
# "més detall" sonor a l'INTRO; els salts del joc i els respawns
|
# "més detall" sonor a l'INTRO; els salts del joc i els respawns
|
||||||
# post-mort cauen al so sec. Buit o absent = sense reverb.
|
# post-mort cauen al so sec. Buit o absent = sense reverb.
|
||||||
land_reverb: stone_hall
|
land_reverb: stone_hall
|
||||||
# So puntual disparat quan apareix el text d'INTRO (phase 2,
|
|
||||||
# text_enter). Es resol contra `data/sounds/<text_sound>`. Buit o
|
|
||||||
# absent = sense so. Es preloda al ctor de GameScene perquè el
|
|
||||||
# primer dispar no faça disc.
|
|
||||||
text_sound: intro_room.wav
|
|
||||||
|
|
||||||
room_outro:
|
room_outro:
|
||||||
text_enter_ms: 400
|
text_enter_ms: 400
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -132,9 +132,12 @@ Director::Director() {
|
|||||||
Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif);
|
Resource::Cache::get().loadSurface(ROOM_TEXT_FONT_GIF_KEY, UiConfig::get().roomTextFont().gif);
|
||||||
Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
Resource::Cache::get().getSurface(ROOM_TEXT_FONT_GIF_KEY)->setPalette(Resource::Cache::get().getPalette("ui"));
|
||||||
|
|
||||||
// Música de la piràmide 1 (primer i únic track de moment). Carreguem ací
|
// Pistes de música del cicle de sala (intro/playing/outro). Es preloden
|
||||||
// perquè forma part del preload d'assets, com la font i la paleta.
|
// ací perquè formen part del preload d'assets, com la font i la paleta;
|
||||||
Resource::Cache::get().loadMusic("pyramid_1.ogg", "data/music/pyramid_1.ogg");
|
// la lògica de quina sona en cada estat viu a `GameScene`.
|
||||||
|
Resource::Cache::get().loadMusic("room_intro.ogg", "data/music/room_intro.ogg");
|
||||||
|
Resource::Cache::get().loadMusic("playing.ogg", "data/music/playing.ogg");
|
||||||
|
Resource::Cache::get().loadMusic("room_outro.ogg", "data/music/room_outro.ogg");
|
||||||
|
|
||||||
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
|
// Catàleg de presets d'efectes per als sons. Es carrega abans d'Audio
|
||||||
// perquè els primers playSoundWith* (al començament del joc) ja
|
// perquè els primers playSoundWith* (al començament del joc) ja
|
||||||
|
|||||||
@@ -179,6 +179,11 @@ void Player::readHurtBlock(const fkyaml::node& yaml) {
|
|||||||
const auto& h = yaml["hurt"];
|
const auto& h = yaml["hurt"];
|
||||||
if (h.contains("speed")) { hurt_config.initial_speed_px_per_s = h["speed"].get_value<float>(); }
|
if (h.contains("speed")) { hurt_config.initial_speed_px_per_s = h["speed"].get_value<float>(); }
|
||||||
if (h.contains("brake_time_ms")) { hurt_config.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
|
if (h.contains("brake_time_ms")) { hurt_config.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
|
||||||
|
// So d'entrada a HURT — mateix patró que `jump.sound` i `whip.sound`.
|
||||||
|
if (h.contains("sound")) {
|
||||||
|
hurt_config.sound_key = h["sound"].get_value<std::string>();
|
||||||
|
Resource::Cache::get().loadSound(hurt_config.sound_key, "data/sounds/" + hurt_config.sound_key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::readWhipBlock(const fkyaml::node& yaml) {
|
void Player::readWhipBlock(const fkyaml::node& yaml) {
|
||||||
@@ -189,6 +194,12 @@ void Player::readWhipBlock(const fkyaml::node& yaml) {
|
|||||||
if (w.contains("swing_ms")) { whip_config.swing_ms = w["swing_ms"].get_value<int>(); }
|
if (w.contains("swing_ms")) { whip_config.swing_ms = w["swing_ms"].get_value<int>(); }
|
||||||
if (w.contains("cooldown_ms")) { whip_config.cooldown_ms = w["cooldown_ms"].get_value<int>(); }
|
if (w.contains("cooldown_ms")) { whip_config.cooldown_ms = w["cooldown_ms"].get_value<int>(); }
|
||||||
if (w.contains("damage")) { whip_config.damage = w["damage"].get_value<int>(); }
|
if (w.contains("damage")) { whip_config.damage = w["damage"].get_value<int>(); }
|
||||||
|
// So del swing: mateix patró que `jump.sound` — basename val de
|
||||||
|
// clau al cache i de path. Si manca, queda buit i no es dispara.
|
||||||
|
if (w.contains("sound")) {
|
||||||
|
whip_config.sound_key = w["sound"].get_value<std::string>();
|
||||||
|
Resource::Cache::get().loadSound(whip_config.sound_key, "data/sounds/" + whip_config.sound_key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::applyCardinal(Cardinal c) {
|
void Player::applyCardinal(Cardinal c) {
|
||||||
@@ -662,6 +673,13 @@ void Player::enterAttacking(Cardinal facing) {
|
|||||||
|
|
||||||
if (facing == Cardinal::NONE) { return; }
|
if (facing == Cardinal::NONE) { return; }
|
||||||
|
|
||||||
|
// So del swing — disparat ací (no des del WhipAttack) perquè és el
|
||||||
|
// jugador qui executa l'acció; si demà el spawn falla per qualsevol
|
||||||
|
// motiu, el so del gest segueix sonant.
|
||||||
|
if (!whip_config.sound_key.empty() && Audio::get() != nullptr) {
|
||||||
|
Audio::get()->playSound(whip_config.sound_key, Audio::Group::GAME);
|
||||||
|
}
|
||||||
|
|
||||||
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
|
// Mapa de Cardinal → int al payload (vegeu `WhipSpawnPayload`).
|
||||||
int dir_int = 0;
|
int dir_int = 0;
|
||||||
switch (facing) {
|
switch (facing) {
|
||||||
@@ -802,6 +820,9 @@ void Player::enterHurt(Cardinal preferred) {
|
|||||||
|
|
||||||
// Activem l'estat i fixem la velocitat segons la direcció triada.
|
// Activem l'estat i fixem la velocitat segons la direcció triada.
|
||||||
state_ = State::HURT;
|
state_ = State::HURT;
|
||||||
|
if (!hurt_config.sound_key.empty() && Audio::get() != nullptr) {
|
||||||
|
Audio::get()->playSound(hurt_config.sound_key, Audio::Group::GAME);
|
||||||
|
}
|
||||||
applyCardinal(CHOSEN);
|
applyCardinal(CHOSEN);
|
||||||
last_input_cardinal_ = CHOSEN;
|
last_input_cardinal_ = CHOSEN;
|
||||||
turning_remaining_ms_ = 0.0F;
|
turning_remaining_ms_ = 0.0F;
|
||||||
|
|||||||
@@ -511,6 +511,11 @@ class Player : public AnimatedSprite {
|
|||||||
struct HurtConfig {
|
struct HurtConfig {
|
||||||
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
||||||
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
||||||
|
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/sounds/`).
|
||||||
|
// Mateixa convenció que la resta de sons del jugador: el basename val
|
||||||
|
// de clau al Resource::Cache i de path. Buit o absent = entrada
|
||||||
|
// silenciosa.
|
||||||
|
std::string sound_key;
|
||||||
};
|
};
|
||||||
static HurtConfig hurt_config;
|
static HurtConfig hurt_config;
|
||||||
|
|
||||||
@@ -529,6 +534,10 @@ class Player : public AnimatedSprite {
|
|||||||
int swing_ms{0};
|
int swing_ms{0};
|
||||||
int cooldown_ms{0};
|
int cooldown_ms{0};
|
||||||
int damage{0};
|
int damage{0};
|
||||||
|
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
|
||||||
|
// `data/sounds/`). Convenció: el basename val tant de clau al
|
||||||
|
// Resource::Cache com de path. Buit o absent = swing silenciós.
|
||||||
|
std::string sound_key;
|
||||||
};
|
};
|
||||||
static WhipConfig whip_config;
|
static WhipConfig whip_config;
|
||||||
|
|
||||||
|
|||||||
@@ -378,11 +378,10 @@ GameScene::GameScene(Config config)
|
|||||||
players_.emplace_back(std::make_unique<Player>(ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
players_.emplace_back(std::make_unique<Player>(ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||||
players_.emplace_back(std::make_unique<Player>(ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
players_.emplace_back(std::make_unique<Player>(ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||||
|
|
||||||
// Tots els slots arrenquen DISABLED. Els actius els reactivarà
|
// Tots els slots arrenquen DISABLED. Els actius els reactivarà el
|
||||||
// `updateRoomTransition` quan s'esgote el delay corresponent dins
|
// callback de fi de música d'INTRO via `completeIntro()` (spawn aeri
|
||||||
// de ROOM_INTRO (`player_spawn_delay_ms` per al primer, i
|
// simultani) en arribar a la fi de la pista `room_intro.ogg`. Els
|
||||||
// `player2_spawn_delay_ms` per al segon en mode BOTH). Els inactius
|
// inactius es queden DISABLED tot el cicle de sala.
|
||||||
// es queden DISABLED tot el cicle de sala.
|
|
||||||
for (auto& p : players_) { p->disable(); }
|
for (auto& p : players_) { p->disable(); }
|
||||||
|
|
||||||
// PaintLayer: font única del pintat del nivell. Es crea després de
|
// PaintLayer: font única del pintat del nivell. Es crea després de
|
||||||
@@ -465,21 +464,12 @@ GameScene::GameScene(Config config)
|
|||||||
// vegada; canvis requereixen reiniciar el joc.
|
// vegada; canvis requereixen reiniciar el joc.
|
||||||
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::CONFIG_YAML);
|
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::CONFIG_YAML);
|
||||||
|
|
||||||
// Preload del so del rètol d'INTRO (no hi ha manifest de sons; el
|
// Cinemàtica d'entrada de la primera sala: arrenca la pista
|
||||||
// pack agafa tot `data/` automàticament i ací registrem la clau
|
// `room_intro.ogg` i arma el callback de fi que farà spawn + salt
|
||||||
// al Resource::Cache perquè Audio::playSound la trobe). Si el camp
|
// a PLAYING. Cal cridar `enterRoomIntro` ací (i no només des
|
||||||
// del YAML està buit, no carreguem res — el dispar consultarà el
|
// d'`advanceToNextRoom`) perquè la primera entrada a la GameScene
|
||||||
// cache i caurà al silenci sense crash.
|
// no passa pel flux d'avanç de sala.
|
||||||
if (!transition_config_.intro_text_sound.empty()) {
|
enterRoomIntro();
|
||||||
Resource::Cache::get().loadSound(
|
|
||||||
transition_config_.intro_text_sound,
|
|
||||||
"data/sounds/" + transition_config_.intro_text_sound);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log d'estat inicial: la FSM arrenca a ROOM_INTRO. En aquesta fita
|
|
||||||
// les transicions són instantànies (sense fades ni text) i el primer
|
|
||||||
// tick d'`updateRoomTransition` ja saltarà a PLAYING.
|
|
||||||
std::cout << "[ROOM] state=ROOM_INTRO (initial)" << '\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GameScene::~GameScene() = default;
|
GameScene::~GameScene() = default;
|
||||||
@@ -563,19 +553,41 @@ void GameScene::enterRoomIntro() {
|
|||||||
}
|
}
|
||||||
state_ = RoomFsm::State::ROOM_INTRO;
|
state_ = RoomFsm::State::ROOM_INTRO;
|
||||||
transition_ = {};
|
transition_ = {};
|
||||||
room_intro_elapsed_ms_ = 0;
|
intro_completed_ = false;
|
||||||
|
intro_spawn_elapsed_ms_ = 0;
|
||||||
players_spawned_in_intro_.fill(false);
|
players_spawned_in_intro_.fill(false);
|
||||||
intro_text_sound_played_ = false;
|
outro_music_ended_ = false;
|
||||||
// La cinemàtica d'entrada és silenciosa per disseny: és INTRO qui
|
|
||||||
// posseeix l'aturada. L'OUTRO no toca la música — així el cicle
|
// INTRO arrenca amb la pista `room_intro.ogg` (loop=0, una sola
|
||||||
// OUTRO → INTRO transmet la sensació de "tall" en entrar a la sala
|
// passada). El callback de fi de música és l'únic responsable de
|
||||||
// nova, no de "fade-out" anterior. Si ja no sona res, stopMusic és
|
// disparar el spawn dels jugadors i el salt a PLAYING — la durada
|
||||||
// no-op.
|
// real ve marcada per la pista, no per cap timer numèric.
|
||||||
if (Audio::get() != nullptr) { Audio::get()->stopMusic(); }
|
auto* audio = Audio::get();
|
||||||
|
if (audio == nullptr) {
|
||||||
|
// Sense Audio inicialitzat, no podem esperar cap callback.
|
||||||
|
// Completem síncronament perquè la sala no quede bloquejada.
|
||||||
|
std::cout << "[ROOM] INTRO: Audio absent — completant síncronament\n";
|
||||||
|
completeIntro();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
audio->stopMusic();
|
||||||
|
audio->setOnMusicEnded([this]() { completeIntro(); });
|
||||||
|
audio->playMusic("room_intro.ogg", 0);
|
||||||
|
if (Audio::getMusicState() != Audio::MusicState::PLAYING) {
|
||||||
|
// Fallback: la pista no s'ha pogut arrencar (resource absent,
|
||||||
|
// música deshabilitada, etc.). Netegem el callback per no
|
||||||
|
// deixar-lo penjat i fem el spawn + transició com si el track
|
||||||
|
// hagués acabat ja. Així cap fallada d'asset deixa la sala
|
||||||
|
// congelada al fons negre de l'INTRO.
|
||||||
|
std::cout << "[ROOM] INTRO: música no arrencada — completant síncronament\n";
|
||||||
|
audio->setOnMusicEnded({});
|
||||||
|
completeIntro();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
||||||
if (slot_index >= players_spawned_in_intro_.size()) { return; }
|
if (slot_index >= players_.size()) { return; }
|
||||||
|
if (!players_starts_active_[slot_index]) { return; }
|
||||||
if (players_spawned_in_intro_[slot_index]) { return; }
|
if (players_spawned_in_intro_[slot_index]) { return; }
|
||||||
players_spawned_in_intro_[slot_index] = true;
|
players_spawned_in_intro_[slot_index] = true;
|
||||||
|
|
||||||
@@ -600,23 +612,76 @@ void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
|||||||
std::cout << "[ROOM] spawn cinematic P" << (slot_index + 1) << ": airborne entry\n";
|
std::cout << "[ROOM] spawn cinematic P" << (slot_index + 1) << ": airborne entry\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameScene::completeIntro() {
|
||||||
|
if (intro_completed_) { return; }
|
||||||
|
intro_completed_ = true;
|
||||||
|
intro_spawn_elapsed_ms_ = 0;
|
||||||
|
// Spawn dels slots amb delay <= 0 (el cas habitual de P1). Els
|
||||||
|
// altres cauen frame a frame dins `updateRoomTransition` a mesura
|
||||||
|
// que `intro_spawn_elapsed_ms_` supera el seu delay. Així P1 cau
|
||||||
|
// alhora que la música acaba i la transició a PLAYING; P2 (en
|
||||||
|
// mode BOTH) entra uns frames més tard amb el seu offset propi.
|
||||||
|
spawnDuePlayers();
|
||||||
|
setState(RoomFsm::State::PLAYING);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::spawnDuePlayers() {
|
||||||
|
if (!intro_completed_) { return; }
|
||||||
|
const bool BOTH_ACTIVE = players_starts_active_[0] && players_starts_active_[1];
|
||||||
|
for (std::size_t i = 0; i < players_starts_active_.size(); ++i) {
|
||||||
|
if (!players_starts_active_[i]) { continue; }
|
||||||
|
if (players_spawned_in_intro_[i]) { continue; }
|
||||||
|
// Single-player: el slot actiu (P1 o P2) sempre fa servir
|
||||||
|
// `player_spawn_delay_ms`. Mode BOTH: P1 fa player_spawn_delay_ms,
|
||||||
|
// P2 fa player2_spawn_delay_ms (offset respecte P1).
|
||||||
|
const int DELAY = (i == 1 && BOTH_ACTIVE)
|
||||||
|
? transition_config_.player2_spawn_delay_ms
|
||||||
|
: transition_config_.player_spawn_delay_ms;
|
||||||
|
if (intro_spawn_elapsed_ms_ >= DELAY) { triggerPlayerSpawn(i); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameScene::setState(RoomFsm::State new_state) {
|
void GameScene::setState(RoomFsm::State new_state) {
|
||||||
if (new_state == state_) { return; }
|
if (new_state == state_) { return; }
|
||||||
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> " << RoomFsm::stateName(new_state) << '\n';
|
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> " << RoomFsm::stateName(new_state) << '\n';
|
||||||
state_ = new_state;
|
state_ = new_state;
|
||||||
transition_ = {};
|
transition_ = {};
|
||||||
// En entrar a un nou cicle d'INTRO, resetegem el timer i el flag de
|
// Reset defensiu dels flags de cicle quan tornem a INTRO via aquest
|
||||||
// l'spawn aeri perquè es torne a disparar al delay configurat.
|
// camí (no canònic — `enterRoomIntro` és el punt d'entrada normal).
|
||||||
if (new_state == RoomFsm::State::ROOM_INTRO) {
|
if (new_state == RoomFsm::State::ROOM_INTRO) {
|
||||||
room_intro_elapsed_ms_ = 0;
|
intro_completed_ = false;
|
||||||
|
intro_spawn_elapsed_ms_ = 0;
|
||||||
players_spawned_in_intro_.fill(false);
|
players_spawned_in_intro_.fill(false);
|
||||||
intro_text_sound_played_ = false;
|
outro_music_ended_ = false;
|
||||||
}
|
}
|
||||||
// En sortir d'INTRO cap a PLAYING, la música arrenca: és el moment
|
auto* audio = Audio::get();
|
||||||
// en què la cinemàtica d'entrada acaba i el jugador agafa control.
|
// PLAYING: arrenca la pista de joc en loop infinit. Netegem el
|
||||||
// L'OUTRO no atura la música; ho fa el següent `enterRoomIntro`.
|
// callback de fi (residual d'INTRO, ja consumit) perquè un futur
|
||||||
if (new_state == RoomFsm::State::PLAYING && Audio::get() != nullptr) {
|
// stopMusic no dispari lògica obsoleta.
|
||||||
Audio::get()->playMusic(config_.music_track, -1);
|
if (new_state == RoomFsm::State::PLAYING && audio != nullptr) {
|
||||||
|
audio->setOnMusicEnded({});
|
||||||
|
audio->playMusic(config_.music_track, -1);
|
||||||
|
}
|
||||||
|
// ROOM_OUTRO: corte sec de `playing.ogg`, arrenca `room_outro.ogg`
|
||||||
|
// (loop=0). El callback de fi marca `outro_music_ended_ = true`,
|
||||||
|
// que junt amb el final del text_exit gata el fade visual a negre
|
||||||
|
// dins `updateRoomTransition`. Si la música no arrenca, marquem el
|
||||||
|
// flag immediatament perquè el fade quedi gata només contra el text.
|
||||||
|
if (new_state == RoomFsm::State::ROOM_OUTRO) {
|
||||||
|
outro_music_ended_ = false;
|
||||||
|
if (audio == nullptr) {
|
||||||
|
std::cout << "[ROOM] OUTRO: Audio absent — fade gata només contra text\n";
|
||||||
|
outro_music_ended_ = true;
|
||||||
|
} else {
|
||||||
|
audio->stopMusic();
|
||||||
|
audio->setOnMusicEnded([this]() { outro_music_ended_ = true; });
|
||||||
|
audio->playMusic("room_outro.ogg", 0);
|
||||||
|
if (Audio::getMusicState() != Audio::MusicState::PLAYING) {
|
||||||
|
std::cout << "[ROOM] OUTRO: música no arrencada — fade gata només contra text\n";
|
||||||
|
audio->setOnMusicEnded({});
|
||||||
|
outro_music_ended_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// L'avanç de sala (OUTRO → INTRO) NO viu ací: el dispara
|
// L'avanç de sala (OUTRO → INTRO) NO viu ací: el dispara
|
||||||
// `updateRoomTransition` quan la FSM detecta el final de l'OUTRO,
|
// `updateRoomTransition` quan la FSM detecta el final de l'OUTRO,
|
||||||
@@ -627,43 +692,38 @@ void GameScene::setState(RoomFsm::State new_state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::updateRoomTransition(int delta_ms) {
|
void GameScene::updateRoomTransition(int delta_ms) {
|
||||||
// Disparador d'spawn aeri: timer propi (independent de les fases de
|
// Tick del timer d'spawn aeri escalonat. Comença al callback de fi
|
||||||
// la FSM) que arrenca a l'entrada de ROOM_INTRO. La separació deixa
|
// de música d'INTRO (`completeIntro` posa intro_completed_=true) i
|
||||||
// el delay desacoblat del fade-in i, demà, reutilitzable per al
|
// segueix corrent cap a PLAYING fins que tots els slots actius hagin
|
||||||
// respawn post-mort sense que la FSM de sala hi haja d'intervenir.
|
// caigut. Cap cost extra un cop tots han caigut (`spawnDuePlayers`
|
||||||
if (state_ == RoomFsm::State::ROOM_INTRO) {
|
// és no-op llavors).
|
||||||
room_intro_elapsed_ms_ += delta_ms;
|
if (intro_completed_) {
|
||||||
// Cada slot actiu pot caure en un moment distint quan els dos
|
intro_spawn_elapsed_ms_ += delta_ms;
|
||||||
// estan actius (mode BOTH): P1 amb player_spawn_delay_ms i P2
|
spawnDuePlayers();
|
||||||
// amb player2_spawn_delay_ms. En single-player (P1 o P2 sols)
|
|
||||||
// l'slot actiu fa servir player_spawn_delay_ms — el segon valor
|
|
||||||
// queda inactiu, com ha demanat el disseny.
|
|
||||||
const bool BOTH_ACTIVE = players_starts_active_[0] && players_starts_active_[1];
|
|
||||||
for (std::size_t i = 0; i < players_starts_active_.size(); ++i) {
|
|
||||||
if (!players_starts_active_[i]) { continue; }
|
|
||||||
if (players_spawned_in_intro_[i]) { continue; }
|
|
||||||
const int DELAY = (i == 1 && BOTH_ACTIVE)
|
|
||||||
? transition_config_.player2_spawn_delay_ms
|
|
||||||
: transition_config_.player_spawn_delay_ms;
|
|
||||||
if (room_intro_elapsed_ms_ >= DELAY) {
|
|
||||||
triggerPlayerSpawn(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto NEXT = RoomFsm::advance(state_, transition_, transition_config_, delta_ms);
|
const auto NEXT = RoomFsm::advance(state_, transition_, transition_config_, delta_ms);
|
||||||
|
|
||||||
// Disparador del so del rètol: la FSM acaba d'avançar; si estem a
|
// OUTRO: gate del fade visual sobre la doble condició (música
|
||||||
// INTRO i ja hem deixat enrere phase 0 (fade_in), reproduïm el so
|
// acabada AND text fora). Si la FSM ha avançat a phase 3 (fade_out)
|
||||||
// una sola vegada. El so es dispara al final del fade-in, anticipant
|
// pel timer del text però el callback de fi de música encara no
|
||||||
// el text que apareix a phase 2 (text_enter): així sembla que el so
|
// ha disparat, congelem el comptador a 0 perquè el fade no comence
|
||||||
// "anuncia" l'arribada del rètol enlloc de coincidir amb la seva
|
// a pintar el negre. Quan el callback alliberi `outro_music_ended_`,
|
||||||
// entrada visual.
|
// les pròximes crides a `advance` tornaran a acumular delta_ms i
|
||||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 1 && !intro_text_sound_played_ && !transition_config_.intro_text_sound.empty() && Audio::get() != nullptr) {
|
// el fade arrencarà des de zero, polit.
|
||||||
Audio::get()->playSound(transition_config_.intro_text_sound, Audio::Group::GAME);
|
if (state_ == RoomFsm::State::ROOM_OUTRO &&
|
||||||
intro_text_sound_played_ = true;
|
transition_.phase == 3 &&
|
||||||
|
!outro_music_ended_) {
|
||||||
|
transition_.elapsed_ms = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NEXT != state_) {
|
if (NEXT != state_) {
|
||||||
|
// INTRO → PLAYING per timer: ignorat. El callback de fi de
|
||||||
|
// música d'INTRO és l'únic responsable d'aquesta transició.
|
||||||
|
if (state_ == RoomFsm::State::ROOM_INTRO &&
|
||||||
|
NEXT == RoomFsm::State::PLAYING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// El final de l'OUTRO no es resol amb un setState normal: cal
|
// El final de l'OUTRO no es resol amb un setState normal: cal
|
||||||
// demanar al LevelManager el següent nivell i carregar-lo abans
|
// demanar al LevelManager el següent nivell i carregar-lo abans
|
||||||
// d'entrar a INTRO. `advanceToNextRoom` fa tot el bloc i acaba
|
// d'entrar a INTRO. `advanceToNextRoom` fa tot el bloc i acaba
|
||||||
|
|||||||
@@ -104,30 +104,52 @@ class GameScene : public Scene {
|
|||||||
// queda DISABLED tot el cicle de sala.
|
// queda DISABLED tot el cicle de sala.
|
||||||
std::array<bool, 2> players_starts_active_{false, false};
|
std::array<bool, 2> players_starts_active_{false, false};
|
||||||
|
|
||||||
// Mil·lisegons des de l'entrada en ROOM_INTRO, per disparar la
|
|
||||||
// cinemàtica de spawn aeri quan s'arriba al delay corresponent al
|
|
||||||
// slot. Es reseteja a `setState(ROOM_INTRO)`. Cada slot té el seu
|
|
||||||
// propi flag de "ja he caigut" — així podem caure en moments
|
|
||||||
// distints en mode BOTH (`player_spawn_delay_ms` per a P1 i
|
|
||||||
// `player2_spawn_delay_ms` per a P2).
|
|
||||||
int room_intro_elapsed_ms_{0};
|
|
||||||
std::array<bool, 2> players_spawned_in_intro_{false, false};
|
|
||||||
|
|
||||||
// One-shot del so del rètol d'INTRO (`text_sound` a game.yaml).
|
|
||||||
// Es disparen quan la FSM passa a phase 2 (text_enter); el flag
|
|
||||||
// evita repeticions si phase 2 s'estén multi-frame. Es reseteja a
|
|
||||||
// `enterRoomIntro` / `setState(ROOM_INTRO)`. Si la clau del so és
|
|
||||||
// buida o el cache no l'ha carregat, queda inutilitzat.
|
|
||||||
bool intro_text_sound_played_{false};
|
|
||||||
|
|
||||||
// Cinemàtica de spawn d'un slot concret. Posa el slot indicat en
|
// Cinemàtica de spawn d'un slot concret. Posa el slot indicat en
|
||||||
// AIRBORNE des d'una altura fora de pantalla. Cridat per
|
// AIRBORNE des d'una altura fora de pantalla. Cridat per
|
||||||
// `updateRoomTransition` quan `room_intro_elapsed_ms_` supera el
|
// `spawnDuePlayers` quan el timer post-callback supera el delay
|
||||||
// delay específic de l'slot. Idempotent (consulta el flag corresponent
|
// del slot. Idempotent (consulta `players_spawned_in_intro_`).
|
||||||
// a `players_spawned_in_intro_`). També és el punt al qual es
|
// També és el punt al qual es connectarà el respawn post-mort en
|
||||||
// connectarà el respawn post-mort en el futur.
|
// el futur.
|
||||||
void triggerPlayerSpawn(std::size_t slot_index);
|
void triggerPlayerSpawn(std::size_t slot_index);
|
||||||
|
|
||||||
|
// Cos del callback de fi de música d'INTRO. Idempotent: si ja s'ha
|
||||||
|
// disparat aquest cicle (`intro_completed_`), no fa res. Marca el
|
||||||
|
// flag, reseteja el timer post-callback i intenta spawn dels slots
|
||||||
|
// actius amb delay <= 0 ara mateix; els altres cauen frame a frame
|
||||||
|
// dins `updateRoomTransition` quan el timer els assoleix. Finalment
|
||||||
|
// commuta a PLAYING — la música del joc arrenca amb el primer slot.
|
||||||
|
void completeIntro();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// callback i la caiguda de l'últim slot). No fa res si ja han
|
||||||
|
// caigut tots o si encara no estem dins la finestra (intro_completed_).
|
||||||
|
void spawnDuePlayers();
|
||||||
|
|
||||||
|
// Hi ha un únic slot global de callback de fi de música a `Ja::Engine`.
|
||||||
|
// Cada estat (INTRO, OUTRO) ha de registrar el seu en entrar i pot
|
||||||
|
// sobreescriure'l amb el del següent estat. Aquests flags marquen
|
||||||
|
// si el callback corresponent ja ha disparat: són la frontera entre
|
||||||
|
// el thread d'audio (que invoca el callback dins `Engine::update`)
|
||||||
|
// i la lògica de transició (`updateRoomTransition`).
|
||||||
|
bool intro_completed_{false};
|
||||||
|
bool outro_music_ended_{false};
|
||||||
|
|
||||||
|
// Mil·lisegons acumulats des del callback de fi de música d'INTRO,
|
||||||
|
// per gatejar l'spawn aeri escalonat dels slots actius. P1 fa
|
||||||
|
// servir `player_spawn_delay_ms` (típicament 0); P2 en mode BOTH
|
||||||
|
// fa servir `player2_spawn_delay_ms` (offset respecte P1). Es
|
||||||
|
// reseteja a 0 dins `completeIntro` i no torna a córrer fins al
|
||||||
|
// següent cicle d'INTRO.
|
||||||
|
int intro_spawn_elapsed_ms_{0};
|
||||||
|
|
||||||
|
// Per slot: ja ha caigut aquest cicle d'INTRO? Garanteix
|
||||||
|
// idempotència de `triggerPlayerSpawn` davant múltiples ticks del
|
||||||
|
// timer mentre el delay no s'ha mogut. Es reseteja a `enterRoomIntro`
|
||||||
|
// i a `setState(ROOM_INTRO)` (camí defensiu no canònic).
|
||||||
|
std::array<bool, 2> players_spawned_in_intro_{false, false};
|
||||||
|
|
||||||
// Vista plana de punters crus — la fa servir Player com a llista
|
// Vista plana de punters crus — la fa servir Player com a llista
|
||||||
// d'obstacles mòbils a la callback de col·lisió. Es construeix un sol
|
// d'obstacles mòbils a la callback de col·lisió. Es construeix un sol
|
||||||
// cop al ctor (els punters són estables perquè `players_` no es
|
// cop al ctor (els punters són estables perquè `players_` no es
|
||||||
@@ -252,10 +274,11 @@ class GameScene : public Scene {
|
|||||||
void advanceToNextRoom();
|
void advanceToNextRoom();
|
||||||
|
|
||||||
// Inicia (o reinicia) el cicle d'entrada de sala: posa l'estat a
|
// Inicia (o reinicia) el cicle d'entrada de sala: posa l'estat a
|
||||||
// ROOM_INTRO i reseteja `transition_`, `room_intro_elapsed_ms_` i el
|
// ROOM_INTRO, reseteja `transition_` i `intro_completed_`, i comença
|
||||||
// flag d'spawn aeri. Idempotent quan ja s'estava a INTRO (els
|
// la pista `room_intro.ogg` amb un callback de fi que dispararà
|
||||||
// timers tornen a començar des de zero — útil per als salts debug
|
// `completeIntro()`. Si la càrrega de la música falla, dispara
|
||||||
// que recarreguen la sala estant ja en INTRO).
|
// `completeIntro()` síncronament perquè la sala no quedi bloquejada.
|
||||||
|
// Idempotent quan ja s'estava a INTRO — útil per als salts debug.
|
||||||
void enterRoomIntro();
|
void enterRoomIntro();
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ namespace RoomFsm {
|
|||||||
if (n.contains("land_reverb")) {
|
if (n.contains("land_reverb")) {
|
||||||
cfg.intro_land_reverb_preset = n["land_reverb"].get_value<std::string>();
|
cfg.intro_land_reverb_preset = n["land_reverb"].get_value<std::string>();
|
||||||
}
|
}
|
||||||
if (n.contains("text_sound")) {
|
|
||||||
cfg.intro_text_sound = n["text_sound"].get_value<std::string>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (yaml.contains("room_outro")) {
|
if (yaml.contains("room_outro")) {
|
||||||
const auto& n = yaml["room_outro"];
|
const auto& n = yaml["room_outro"];
|
||||||
|
|||||||
@@ -47,23 +47,19 @@ namespace RoomFsm {
|
|||||||
// sense recompilar — útil per donar respir abans del nou rètol.
|
// sense recompilar — útil per donar respir abans del nou rètol.
|
||||||
int outro_black_hold_ms{0};
|
int outro_black_hold_ms{0};
|
||||||
|
|
||||||
// Mil·lisegons des de l'inici de ROOM_INTRO en què cau la
|
// Mil·lisegons des del callback de fi de la música d'INTRO
|
||||||
// cinemàtica d'spawn aeri dels jugadors actius. Específic per a
|
// (`room_intro.ogg`) en què cau l'spawn aeri del primer slot
|
||||||
// l'entrada de sala — el respawn post-mort tindrà el seu propi
|
// actiu. 0 = cau al moment exacte que la música acaba. El
|
||||||
// timing quan toqui dissenyar-lo. Un valor habitual: la suma de
|
// respawn post-mort tindrà el seu propi timing quan toqui
|
||||||
// fade_in + text_delay + text_enter + text_hold, perquè els
|
// dissenyar-lo.
|
||||||
// players caiguin just quan el rètol comença a sortir. El
|
int player_spawn_delay_ms{0};
|
||||||
// fallback compilat (500 ms) deixa el spawn al final del fade-in
|
|
||||||
// si el YAML no defineix res — només pensat com a degradació.
|
|
||||||
int player_spawn_delay_ms{500};
|
|
||||||
|
|
||||||
// Delay independent per al segon jugador, **només quan els dos
|
// Delay independent per al segon jugador, **només quan els dos
|
||||||
// slots estan actius** (mode BOTH). En single-player (P1 o P2
|
// slots estan actius** (mode BOTH). En single-player (P1 o P2
|
||||||
// sols) s'usa sempre `player_spawn_delay_ms` per al jugador
|
// sols) s'usa sempre `player_spawn_delay_ms`. Si el YAML no
|
||||||
// actiu. Si el YAML no fixa aquest valor, el `loadConfig` el
|
// fixa aquest valor, `loadConfig` el copia de `player_spawn_delay_ms`
|
||||||
// copia de `player_spawn_delay_ms` perquè els dos players caiguin
|
// perquè els dos players caiguin alhora — comportament neutre.
|
||||||
// alhora — comportament històric del joc.
|
int player2_spawn_delay_ms{0};
|
||||||
int player2_spawn_delay_ms{500};
|
|
||||||
|
|
||||||
// Píxels extra entre les dues línies del text d'INTRO (piràmide
|
// Píxels extra entre les dues línies del text d'INTRO (piràmide
|
||||||
// damunt, sala davall). Es sumen al `lineHeight` natural del
|
// damunt, sala davall). Es sumen al `lineHeight` natural del
|
||||||
@@ -87,13 +83,6 @@ namespace RoomFsm {
|
|||||||
// cinemàtica d'entrada sona igual que un salt qualsevol).
|
// cinemàtica d'entrada sona igual que un salt qualsevol).
|
||||||
std::string intro_land_reverb_preset;
|
std::string intro_land_reverb_preset;
|
||||||
|
|
||||||
// Clau d'un so puntual (data/sounds/<key>) que GameScene
|
|
||||||
// dispara just quan la cinemàtica d'INTRO arriba a la fase
|
|
||||||
// d'entrada del text (phase == 2). Buit o absent = sense so.
|
|
||||||
// Convenció Resource::Cache: el basename val tant de clau com
|
|
||||||
// de path (loadSound ho fa servir igual que el land_sound).
|
|
||||||
std::string intro_text_sound;
|
|
||||||
|
|
||||||
// Panel translúcid sota el rètol de transició (INTRO i OUTRO).
|
// Panel translúcid sota el rètol de transició (INTRO i OUTRO).
|
||||||
// Centrat verticalment al centre del playfield, ocupa tota
|
// Centrat verticalment al centre del playfield, ocupa tota
|
||||||
// l'amplada del canvas. Creix d'altura 0 → height_px durant la
|
// l'amplada del canvas. Creix d'altura 0 → height_px durant la
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ namespace Defaults {
|
|||||||
// així que Player::loadFrom només cal cridar-la amb un dels dos YAMLs.
|
// així que Player::loadFrom només cal cridar-la amb un dels dos YAMLs.
|
||||||
constexpr const char* PLAYER_SPRITE_P2 = "data/player/morcus.gif";
|
constexpr const char* PLAYER_SPRITE_P2 = "data/player/morcus.gif";
|
||||||
constexpr const char* PLAYER_YAML_P2 = "data/player/morcus.yaml";
|
constexpr const char* PLAYER_YAML_P2 = "data/player/morcus.yaml";
|
||||||
constexpr const char* MUSIC_TRACK = "pyramid_1.ogg";
|
constexpr const char* MUSIC_TRACK = "playing.ogg";
|
||||||
} // namespace Pyramid
|
} // namespace Pyramid
|
||||||
|
|
||||||
} // namespace Defaults
|
} // namespace Defaults
|
||||||
Reference in new issue
Block a user