Lligua el reverb al moment de la cinemàtica d'entrada de sala, no als
salts en general. La sala dóna "més detall" sonor a l'INTRO; un cop el
joc està en marxa, els salts (i els respawns post-mort) tornen al so
sec. Així el primer aterratge es percep com a part de l'animació
d'arribada, no com una propietat permanent del personatge.
- RoomFsm::Config: nou camp `intro_land_reverb_preset` carregat des
de `room_intro.land_reverb` a `data/scenes/game.yaml` (per defecte
'stone_hall'). Buit = sense reverb a l'INTRO.
- Player: membre `next_land_reverb_preset_` consumit per
`integrateJump` al primer aterratge que el trobe armat. Setter
públic `armNextLandReverb(preset)` per a GameScene. Cap altre punt
(respawn, exit, disable) el toca: el reverb és estrictament
one-shot lligat al spawn aeri d'INTRO.
- GameScene::triggerPlayerSpawn: arma el preset al jugador just
després de `spawnFromAir`, així cada slot rep el seu reverb un
sol cop per cinemàtica.
- JumpConfig perd `land_reverb_preset` i els YAMLs de Sam/Morcus
perden `land_reverb`: la decisió ja no viu al personatge.
172 lines
7.5 KiB
C++
172 lines
7.5 KiB
C++
#include "game/scenes/room_fsm.hpp"
|
|
|
|
#include <array>
|
|
#include <iostream>
|
|
#include <string_view>
|
|
|
|
#include "core/locale/locale.hpp"
|
|
#include "core/resources/resource_helper.hpp"
|
|
#include "external/fkyaml_node.hpp"
|
|
|
|
namespace {
|
|
|
|
// Llegeix un sub-camp sencer d'un node YAML. Si manca, retorna el
|
|
// fallback. Dissenyat per al consum puntual de `loadConfig`.
|
|
auto readChildInt(const fkyaml::node& parent, const char* key, int fallback) -> int {
|
|
if (!parent.contains(key)) { return fallback; }
|
|
return parent[key].get_value<int>();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace RoomFsm {
|
|
|
|
auto loadConfig(const std::string& yaml_path) -> Config {
|
|
Config cfg{};
|
|
cfg.text_enter_curve = UiConfig::curveByName("outCubic");
|
|
cfg.text_exit_curve = UiConfig::curveByName("inCubic");
|
|
|
|
auto bytes = Resource::Helper::loadFile(yaml_path);
|
|
if (bytes.empty()) {
|
|
std::cout << "RoomFsm: " << yaml_path << " absent — uso fallbacks compilats\n";
|
|
return cfg;
|
|
}
|
|
const auto* begin = reinterpret_cast<const char*>(bytes.data());
|
|
const auto* end = begin + bytes.size();
|
|
auto yaml = fkyaml::node::deserialize(begin, end);
|
|
|
|
if (yaml.contains("room_intro")) {
|
|
const auto& n = yaml["room_intro"];
|
|
cfg.intro_fade_in_ms = readChildInt(n, "fade_in_ms", cfg.intro_fade_in_ms);
|
|
cfg.intro_text_delay_ms = readChildInt(n, "text_delay_ms", cfg.intro_text_delay_ms);
|
|
cfg.intro_text_enter_ms = readChildInt(n, "text_enter_ms", cfg.intro_text_enter_ms);
|
|
cfg.intro_text_hold_ms = readChildInt(n, "text_hold_ms", cfg.intro_text_hold_ms);
|
|
cfg.intro_text_exit_ms = readChildInt(n, "text_exit_ms", cfg.intro_text_exit_ms);
|
|
cfg.player_spawn_delay_ms = readChildInt(n, "player_spawn_delay_ms", cfg.player_spawn_delay_ms);
|
|
// Per defecte el segon jugador cau alhora que el primer.
|
|
// El YAML pot trencar la simultaneïtat fixant un valor propi.
|
|
cfg.player2_spawn_delay_ms = readChildInt(n, "player2_spawn_delay_ms", cfg.player_spawn_delay_ms);
|
|
cfg.intro_text_line_gap_px = readChildInt(n, "line_gap_px", cfg.intro_text_line_gap_px);
|
|
if (n.contains("land_reverb")) {
|
|
cfg.intro_land_reverb_preset = n["land_reverb"].get_value<std::string>();
|
|
}
|
|
}
|
|
if (yaml.contains("room_outro")) {
|
|
const auto& n = yaml["room_outro"];
|
|
cfg.outro_text_enter_ms = readChildInt(n, "text_enter_ms", cfg.outro_text_enter_ms);
|
|
cfg.outro_text_hold_ms = readChildInt(n, "text_hold_ms", cfg.outro_text_hold_ms);
|
|
cfg.outro_text_exit_ms = readChildInt(n, "text_exit_ms", cfg.outro_text_exit_ms);
|
|
cfg.outro_fade_out_ms = readChildInt(n, "fade_out_ms", cfg.outro_fade_out_ms);
|
|
cfg.outro_text_line_gap_px = readChildInt(n, "line_gap_px", cfg.outro_text_line_gap_px);
|
|
cfg.outro_black_hold_ms = readChildInt(n, "black_hold_ms", cfg.outro_black_hold_ms);
|
|
}
|
|
if (yaml.contains("text")) {
|
|
const auto& n = yaml["text"];
|
|
if (n.contains("enter_curve")) {
|
|
cfg.text_enter_curve = UiConfig::curveByName(n["enter_curve"].get_value<std::string>());
|
|
}
|
|
if (n.contains("exit_curve")) {
|
|
cfg.text_exit_curve = UiConfig::curveByName(n["exit_curve"].get_value<std::string>());
|
|
}
|
|
}
|
|
if (yaml.contains("text_panel")) {
|
|
const auto& n = yaml["text_panel"];
|
|
if (n.contains("color") && n["color"].is_sequence() && n["color"].size() == 3) {
|
|
const auto R = n["color"][0].get_value<std::uint32_t>() & 0xFFU;
|
|
const auto G = n["color"][1].get_value<std::uint32_t>() & 0xFFU;
|
|
const auto B = n["color"][2].get_value<std::uint32_t>() & 0xFFU;
|
|
cfg.text_panel_color = (R << 16) | (G << 8) | B;
|
|
}
|
|
if (n.contains("alpha")) {
|
|
cfg.text_panel_alpha = static_cast<std::uint8_t>(n["alpha"].get_value<std::uint32_t>() & 0xFFU);
|
|
}
|
|
cfg.text_panel_height_px = readChildInt(n, "height_px", cfg.text_panel_height_px);
|
|
}
|
|
|
|
std::cout << "RoomFsm: timings carregats des de " << yaml_path
|
|
<< " (intro fade " << cfg.intro_fade_in_ms
|
|
<< "ms, outro fade " << cfg.outro_fade_out_ms << "ms)\n";
|
|
return cfg;
|
|
}
|
|
|
|
auto advance(State current, Transition& t, const Config& cfg, int delta_ms) -> State {
|
|
if (current == State::PLAYING) { return current; }
|
|
|
|
t.elapsed_ms += delta_ms;
|
|
|
|
if (current == State::ROOM_INTRO) {
|
|
const std::array<int, 5> DURS = {
|
|
cfg.intro_fade_in_ms,
|
|
cfg.intro_text_delay_ms,
|
|
cfg.intro_text_enter_ms,
|
|
cfg.intro_text_hold_ms,
|
|
cfg.intro_text_exit_ms,
|
|
};
|
|
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
|
t.elapsed_ms -= DURS[t.phase];
|
|
++t.phase;
|
|
}
|
|
if (t.phase >= static_cast<int>(DURS.size())) { return State::PLAYING; }
|
|
return current;
|
|
}
|
|
|
|
// ROOM_OUTRO
|
|
const std::array<int, 5> DURS = {
|
|
cfg.outro_text_enter_ms,
|
|
cfg.outro_text_hold_ms,
|
|
cfg.outro_text_exit_ms,
|
|
cfg.outro_fade_out_ms,
|
|
// Pausa final en negre, després del fade-out i abans del
|
|
// canvi a INTRO. Si és 0 ms, el while-loop la consumeix
|
|
// sense esperar — comportament idèntic a l'històric.
|
|
cfg.outro_black_hold_ms,
|
|
};
|
|
while (t.phase < static_cast<int>(DURS.size()) && t.elapsed_ms >= DURS[t.phase]) {
|
|
t.elapsed_ms -= DURS[t.phase];
|
|
++t.phase;
|
|
}
|
|
// Final de l'OUTRO → ROOM_INTRO de la sala SEGÜENT. La FSM no
|
|
// sap quina és — només avisa del canvi d'estat. La GameScene,
|
|
// dins `setState(ROOM_INTRO)` amb PREV=OUTRO, demana al
|
|
// LevelManager el següent nivell i recarrega tilemap/entitats.
|
|
if (t.phase >= static_cast<int>(DURS.size())) { return State::ROOM_INTRO; }
|
|
return current;
|
|
}
|
|
|
|
namespace {
|
|
// Substitueix la primera ocurrència de `placeholder` dins de `tpl`
|
|
// pel valor d'`n` convertit a string. Utilitzat per les dues
|
|
// formatadores d'INTRO; si la clau del Locale no porta el
|
|
// placeholder (traducció buida o ja substituïda), retorna la
|
|
// plantilla intacta.
|
|
auto substitute(std::string tpl, std::string_view placeholder, int n) -> std::string {
|
|
auto pos = tpl.find(placeholder);
|
|
if (pos != std::string::npos) {
|
|
tpl.replace(pos, placeholder.size(), std::to_string(n));
|
|
}
|
|
return tpl;
|
|
}
|
|
} // namespace
|
|
|
|
auto formatIntroPyramidLine(int pyramid) -> std::string {
|
|
return substitute(Locale::get().text("game.room_intro_pyramid"), "{p}", pyramid);
|
|
}
|
|
|
|
auto formatIntroRoomLine(int room) -> std::string {
|
|
return substitute(Locale::get().text("game.room_intro_room"), "{r}", room);
|
|
}
|
|
|
|
auto stateName(State s) -> const char* {
|
|
switch (s) {
|
|
case State::ROOM_INTRO:
|
|
return "ROOM_INTRO";
|
|
case State::PLAYING:
|
|
return "PLAYING";
|
|
case State::ROOM_OUTRO:
|
|
return "ROOM_OUTRO";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
} // namespace RoomFsm
|