diff --git a/data/scenes/game.yaml b/data/scenes/game.yaml index b91a020..aca1ef4 100644 --- a/data/scenes/game.yaml +++ b/data/scenes/game.yaml @@ -5,16 +5,17 @@ # fallbacks compilats; veure `readSceneConfig` a game_scene.cpp. room_intro: - fade_in_ms: 500 # negre opac → món visible - text_delay_ms: 500 # espera amb el món net abans de sortir el text - text_enter_ms: 400 # text esquerra → centre - text_hold_ms: 1500 # text quiet al centre - text_exit_ms: 400 # text centre → fora per l'esquerra - line_gap_px: -4 # px extra entre les dues línies (piràmide / sala) + fade_in_ms: 500 # negre opac → món visible + text_delay_ms: 500 # espera amb el món net abans de sortir el text + text_enter_ms: 400 # text esquerra → centre + text_hold_ms: 2000 # text quiet al centre + text_exit_ms: 400 # text centre → fora per l'esquerra + 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. room_outro: text_enter_ms: 400 - text_hold_ms: 1500 + text_hold_ms: 2000 text_exit_ms: 400 fade_out_ms: 500 # món visible → negre opac line_gap_px: -4 # px extra entre les dues línies (fase / completada) @@ -25,3 +26,13 @@ text: # `data/ui.yaml`). El text entra suau i surt accelerant. enter_curve: outCubic exit_curve: inCubic + +# Panel translúcid pintat sota el rètol de transició (INTRO i OUTRO). +# Va centrat verticalment al centre del playfield i ocupa tota +# l'amplada del canvas. Creix d'altura amb la mateixa enter_curve del +# text i decreix amb l'exit_curve. `color` són tres canals RGB (0..255); +# `alpha` és la transparència del blend (0 = invisible, 255 = opac). +text_panel: + color: [36, 182, 170] + alpha: 128 + height_px: 32 diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 70057c6..d275a47 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -640,6 +640,10 @@ void GameScene::renderRoomFade() { Screen::get().blendRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, 0x00000000U, ALPHA); } +// TEMPORAL — al pròxim commit s'extrauen compute_x, compute_panel_height, +// draw_panel i draw_line a mètodes privats; aquest NOLINT és només per +// "guardar feina" mentre encara estem fent ajustos visuals. +// NOLINTNEXTLINE(readability-function-cognitive-complexity) void GameScene::renderRoomText() { // Text de la transició de sala. Font dedicada (`room_text_font_`, // CaesarDressing per defecte) — la pausa té la seua i no es barregen. @@ -699,6 +703,41 @@ void GameScene::renderRoomText() { return GameCanvas::CENTER_X; }; + // Panel translúcid sota el rètol: l'altura s'anima de 0 a + // `text_panel_height_px` durant l'enter (amb la mateixa + // text_enter_curve que els textos), es manté plena durant el hold + // i decreix durant l'exit. Cada estat (INTRO, OUTRO) li passa la + // tripleta de fases (enter, hold, exit) i les seues durades. + auto compute_panel_height = [&](int p_enter, int p_hold, int p_exit, int d_enter, int d_exit) -> int { + const int MAX = transition_config_.text_panel_height_px; + if (transition_.phase == p_enter) { + const float T = (d_enter > 0) + ? std::clamp(static_cast(transition_.elapsed_ms) / static_cast(d_enter), 0.0F, 1.0F) + : 1.0F; + const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear; + return Easing::lerpInt(0, MAX, CURVE(T)); + } + if (transition_.phase == p_hold) { return MAX; } + if (transition_.phase == p_exit) { + const float T = (d_exit > 0) + ? std::clamp(static_cast(transition_.elapsed_ms) / static_cast(d_exit), 0.0F, 1.0F) + : 1.0F; + const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear; + return Easing::lerpInt(MAX, 0, CURVE(T)); + } + return 0; + }; + + auto draw_panel = [&](int panel_height) { + if (panel_height <= 0 || transition_config_.text_panel_alpha == 0) { return; } + Screen::get().blendRect(0, + PLAY_CENTER_Y - (panel_height / 2), + GameCanvas::WIDTH, + panel_height, + transition_config_.text_panel_color, + transition_config_.text_panel_alpha); + }; + auto draw_line = [&](const std::string& text, int x, int y) { room_text_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG, x, @@ -711,6 +750,9 @@ void GameScene::renderRoomText() { }; if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) { + // Panel primer (queda sota el text gràcies a l'ordre de blits). + draw_panel(compute_panel_height(2, 3, 4, transition_config_.intro_text_enter_ms, transition_config_.intro_text_exit_ms)); + const auto& mgr = Levels::LevelManager::get(); const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid()); const std::string LINE_ROOM = RoomFsm::formatIntroRoomLine(mgr.currentRoom()); @@ -735,6 +777,8 @@ void GameScene::renderRoomText() { draw_line(LINE_PYRAMID, X_PYRAMID, Y_PYRAMID); draw_line(LINE_ROOM, X_ROOM, Y_ROOM); } else if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase >= 0 && transition_.phase <= 2) { + draw_panel(compute_panel_height(0, 1, 2, transition_config_.outro_text_enter_ms, transition_config_.outro_text_exit_ms)); + // Mateix patró que l'INTRO: dues línies en direccions oposades. // Línia 1 ("Fase") per l'esquerra, línia 2 ("Completada") per // la dreta; surten cap al costat oposat del que han entrat. diff --git a/source/game/scenes/room_fsm.cpp b/source/game/scenes/room_fsm.cpp index 7c28a08..e6f87e6 100644 --- a/source/game/scenes/room_fsm.cpp +++ b/source/game/scenes/room_fsm.cpp @@ -63,6 +63,19 @@ namespace RoomFsm { cfg.text_exit_curve = UiConfig::curveByName(n["exit_curve"].get_value()); } } + 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() & 0xFFU; + const auto G = n["color"][1].get_value() & 0xFFU; + const auto B = n["color"][2].get_value() & 0xFFU; + cfg.text_panel_color = (R << 16) | (G << 8) | B; + } + if (n.contains("alpha")) { + cfg.text_panel_alpha = static_cast(n["alpha"].get_value() & 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 diff --git a/source/game/scenes/room_fsm.hpp b/source/game/scenes/room_fsm.hpp index f3dc446..9b7af74 100644 --- a/source/game/scenes/room_fsm.hpp +++ b/source/game/scenes/room_fsm.hpp @@ -48,10 +48,13 @@ namespace RoomFsm { int outro_black_hold_ms{0}; // Mil·lisegons des de l'inici de ROOM_INTRO en què cau la - // cinemàtica d'spawn aeri dels jugadors actius. Independent del - // fade-in perquè el mateix valor servirà per al respawn post-mort - // (que no té fade que el sincronitze). 500 ms = típicament el - // moment en què el fade-in acaba i el text encara no ha entrat. + // cinemàtica d'spawn aeri dels jugadors actius. Específic per a + // l'entrada de sala — el respawn post-mort tindrà el seu propi + // timing quan toqui dissenyar-lo. Un valor habitual: la suma de + // fade_in + text_delay + text_enter + text_hold, perquè els + // players caiguin just quan el rètol comença a sortir. El + // 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}; // Píxels extra entre les dues línies del text d'INTRO (piràmide @@ -68,6 +71,17 @@ namespace RoomFsm { UiConfig::CurveFn text_enter_curve{nullptr}; UiConfig::CurveFn text_exit_curve{nullptr}; + + // Panel translúcid sota el rètol de transició (INTRO i OUTRO). + // Centrat verticalment al centre del playfield, ocupa tota + // l'amplada del canvas. Creix d'altura 0 → height_px durant la + // fase d'enter del text (amb `text_enter_curve`) i decreix + // durant la fase d'exit (amb `text_exit_curve`). 0 d'alpha o + // 0 d'alçada el deixa invisible. Color RGB (sense alpha — la + // transparència viu al camp `alpha`). + std::uint32_t text_panel_color{0x000000U}; + std::uint8_t text_panel_alpha{128}; + int text_panel_height_px{40}; }; // Llegeix la configuració des del YAML. Si el fitxer manca o falten