From 99eb42b3197d5373e6198c584064bcc84febb1fb Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 26 Apr 2026 18:44:49 +0200 Subject: [PATCH] refactor(game-scene): extreu helpers de renderRoomText i elimina el NOLINT --- source/game/scenes/game_scene.cpp | 260 +++++++++++++++--------------- source/game/scenes/game_scene.hpp | 12 ++ 2 files changed, 142 insertions(+), 130 deletions(-) diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index d275a47..bccea33 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -640,165 +640,165 @@ 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. - // Patró esquerra → centre → dreta amb les durades pròpies de la - // transició. + // Orquestrador del rètol de transició de sala. La feina pesada + // (animació de X, animació d'altura del panel i pintat) la fan + // quatre helpers privats; aquesta funció només decideix quin + // text dibuixar i amb quines fases segons l'estat. // // Mapatge per estat (només els estats amb text actiu hi entren): // ROOM_INTRO: phases 2..4 → enter / hold / exit; dues línies - // (piràmide damunt, sala davall) que comparteixen - // animació però es dibuixen amb writeDX independents - // per deixar la porta oberta a desacoblar-les. + // (piràmide damunt, sala davall) que travessen la + // pantalla en direccions oposades. // ROOM_OUTRO: phases 0..2 → enter / hold / exit; dues línies // oposades ("fase" / "completada"). Phase 3 és el // fade-out i 4 la pausa en negre — cap text en cap // de les dues. if (!room_text_font_) { return; } + constexpr int KERNING = 0; // Centre vertical de la zona de joc (canvas sense la HUD). Reemplaça // l'antic GameCanvas::CENTER_Y per descomptar la franja del marcador // i tenir el rètol equilibrat sobre el playfield. const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2; - - constexpr int KERNING = 0; - constexpr Uint8 STROKE_DIST = 1; - // Colors del rètol via UiConfig — abans el contorn estava - // hardcodejat a 255. Ara `data/ui.yaml > colors.room_text` i - // `room_text_outline` controlen relleno i contorn sense afectar - // les notificacions del HUD. - const Uint8 FILL_COLOR = UiConfig::get().roomText(); - const Uint8 OUTLINE_COLOR = UiConfig::get().roomTextOutline(); - - // Calcula la X animada d'una línia: entra des de fora de pantalla, - // s'atura al CENTER_X durant el hold i surt pel costat oposat. - // `from_right=false` és el patró clàssic (entra per l'esquerra, - // surt cap a la dreta); `true` el flipa (entra per la dreta, surt - // cap a l'esquerra). Així les dues línies de l'INTRO poden - // travessar la pantalla en direccions oposades. - auto compute_x = [&](int width, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) -> int { - const int OFF_LEFT = -width; - const int OFF_RIGHT = GameCanvas::WIDTH; - const int OFF_ENTER = from_right ? OFF_RIGHT : OFF_LEFT; - const int OFF_EXIT = from_right ? OFF_LEFT : OFF_RIGHT; - if (transition_.phase == phase_enter) { - const float T = (dur_enter > 0) - ? std::clamp(static_cast(transition_.elapsed_ms) / static_cast(dur_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(OFF_ENTER, GameCanvas::CENTER_X, CURVE(T)); - } - if (transition_.phase == phase_exit) { - const float T = (dur_exit > 0) - ? std::clamp(static_cast(transition_.elapsed_ms) / static_cast(dur_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(GameCanvas::CENTER_X, OFF_EXIT, CURVE(T)); - } - 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, - y, - text, - KERNING, - FILL_COLOR, - STROKE_DIST, - OUTLINE_COLOR); - }; + // Colors del rètol via UiConfig (`data/ui.yaml > colors.room_text` i + // `room_text_outline`). Independents dels colors de notificació. + const Uint8 FILL = UiConfig::get().roomText(); + const Uint8 OUTLINE = UiConfig::get().roomTextOutline(); + const int LINE_HEIGHT = room_text_font_->getLineHeight(); if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) { + const int DUR_ENTER = transition_config_.intro_text_enter_ms; + const int DUR_EXIT = transition_config_.intro_text_exit_ms; // 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)); + drawRoomTextPanel(PLAY_CENTER_Y, + computeRoomTextPanelHeight(2, 3, 4, DUR_ENTER, DUR_EXIT)); const auto& mgr = Levels::LevelManager::get(); const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid()); const std::string LINE_ROOM = RoomFsm::formatIntroRoomLine(mgr.currentRoom()); - const int W_PYRAMID = room_text_font_->length(LINE_PYRAMID, KERNING); - const int W_ROOM = room_text_font_->length(LINE_ROOM, KERNING); - const int DUR_ENTER = transition_config_.intro_text_enter_ms; - const int DUR_EXIT = transition_config_.intro_text_exit_ms; - // Direccions oposades: la línia de la piràmide entra per - // l'esquerra, la de la sala per la dreta. Quan surten, cada - // una continua cap al costat oposat al que ha entrat. - const int X_PYRAMID = compute_x(W_PYRAMID, 2, 4, DUR_ENTER, DUR_EXIT, /*from_right=*/false); - const int X_ROOM = compute_x(W_ROOM, 2, 4, DUR_ENTER, DUR_EXIT, /*from_right=*/true); - // Bloc de dues línies centrat al voltant de CENTER_Y. Entre - // les dues línies hi posem `intro_text_line_gap_px` extra (a - // més del lineHeight natural) per donar-los aire visual; la - // meitat la traiem cap amunt de la piràmide i la meitat la - // sumem cap avall a la sala, mantenint el centre del bloc fix. - const int LINE_HEIGHT = room_text_font_->getLineHeight(); + // Direccions oposades: piràmide entra per l'esquerra i surt per + // la dreta; sala fa el contrari. + const int X_PYRAMID = computeRoomTextX(room_text_font_->length(LINE_PYRAMID, KERNING), + 2, + 4, + DUR_ENTER, + DUR_EXIT, + /*from_right=*/false); + const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING), + 2, + 4, + DUR_ENTER, + DUR_EXIT, + /*from_right=*/true); + // Bloc de dues línies centrat al voltant del PLAY_CENTER_Y. El + // gap (`intro_text_line_gap_px`) es reparteix simètricament: + // mig amunt a la piràmide, mig avall a la sala. const int GAP = transition_config_.intro_text_line_gap_px; - const int Y_PYRAMID = PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2); - const int Y_ROOM = PLAY_CENTER_Y + (GAP / 2); - draw_line(LINE_PYRAMID, X_PYRAMID, Y_PYRAMID); - draw_line(LINE_ROOM, X_ROOM, Y_ROOM); + drawRoomTextLine(LINE_PYRAMID, X_PYRAMID, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE); + drawRoomTextLine(LINE_ROOM, X_ROOM, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE); } 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. - const std::string LINE_1 = Locale::get().text("game.room_complete_line1"); - const std::string LINE_2 = Locale::get().text("game.room_complete_line2"); - const int W_1 = room_text_font_->length(LINE_1, KERNING); - const int W_2 = room_text_font_->length(LINE_2, KERNING); const int DUR_ENTER = transition_config_.outro_text_enter_ms; const int DUR_EXIT = transition_config_.outro_text_exit_ms; - const int X_1 = compute_x(W_1, 0, 2, DUR_ENTER, DUR_EXIT, /*from_right=*/false); - const int X_2 = compute_x(W_2, 0, 2, DUR_ENTER, DUR_EXIT, /*from_right=*/true); - const int LINE_HEIGHT = room_text_font_->getLineHeight(); + drawRoomTextPanel(PLAY_CENTER_Y, + computeRoomTextPanelHeight(0, 1, 2, DUR_ENTER, DUR_EXIT)); + + const std::string LINE_1 = Locale::get().text("game.room_complete_line1"); + const std::string LINE_2 = Locale::get().text("game.room_complete_line2"); + const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING), + 0, + 2, + DUR_ENTER, + DUR_EXIT, + /*from_right=*/false); + const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING), + 0, + 2, + DUR_ENTER, + DUR_EXIT, + /*from_right=*/true); const int GAP = transition_config_.outro_text_line_gap_px; - const int Y_1 = PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2); - const int Y_2 = PLAY_CENTER_Y + (GAP / 2); - draw_line(LINE_1, X_1, Y_1); - draw_line(LINE_2, X_2, Y_2); + drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE); + drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE); } } +auto GameScene::computeRoomTextX(int width, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int { + // X animada d'una línia: entra des de fora de pantalla, s'atura al + // CENTER_X durant el hold (fora del rang d'aquesta funció — el caller + // garanteix que cau a la branca return final) i surt pel costat + // oposat. `from_right=false` és el patró clàssic (esquerra → dreta); + // `true` el flipa. + const int OFF_LEFT = -width; + const int OFF_RIGHT = GameCanvas::WIDTH; + const int OFF_ENTER = from_right ? OFF_RIGHT : OFF_LEFT; + const int OFF_EXIT = from_right ? OFF_LEFT : OFF_RIGHT; + if (transition_.phase == phase_enter) { + const float T = (dur_enter > 0) + ? std::clamp(static_cast(transition_.elapsed_ms) / static_cast(dur_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(OFF_ENTER, GameCanvas::CENTER_X, CURVE(T)); + } + if (transition_.phase == phase_exit) { + const float T = (dur_exit > 0) + ? std::clamp(static_cast(transition_.elapsed_ms) / static_cast(dur_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(GameCanvas::CENTER_X, OFF_EXIT, CURVE(T)); + } + return GameCanvas::CENTER_X; +} + +auto GameScene::computeRoomTextPanelHeight(int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int { + // Altura animada del panel translúcid sota el rètol. Creix de 0 a + // `text_panel_height_px` amb `text_enter_curve` durant l'enter, es + // manté plena durant el hold i decreix amb `text_exit_curve` + // durant l'exit. Fora d'aquestes tres fases retorna 0. + const int MAX = transition_config_.text_panel_height_px; + if (transition_.phase == phase_enter) { + const float T = (dur_enter > 0) + ? std::clamp(static_cast(transition_.elapsed_ms) / static_cast(dur_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 == phase_hold) { return MAX; } + if (transition_.phase == phase_exit) { + const float T = (dur_exit > 0) + ? std::clamp(static_cast(transition_.elapsed_ms) / static_cast(dur_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; +} + +void GameScene::drawRoomTextPanel(int center_y, int panel_height) const { + // No-op si l'altura és nul·la o l'alpha està a zero — així evitem + // un blendRect inútil i deixem el caller cridar incondicionalment. + if (panel_height <= 0 || transition_config_.text_panel_alpha == 0) { return; } + Screen::get().blendRect(0, + center_y - (panel_height / 2), + GameCanvas::WIDTH, + panel_height, + transition_config_.text_panel_color, + transition_config_.text_panel_alpha); +} + +void GameScene::drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const { + constexpr int KERNING = 0; + constexpr Uint8 STROKE_DIST = 1; + room_text_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG, + x, + y, + text, + KERNING, + fill_color, + STROKE_DIST, + outline_color); +} + void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) { // Mida del sprite de Dust llegida del seu YAML. Hardcoded per // simplificar — si canvia, només cal tocar dust.yaml + aquesta diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index e17ed67..33cfde9 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -196,6 +196,18 @@ class GameScene : public Scene { void renderRoomFade(); void renderRoomText(); + // Helpers de `renderRoomText`. Tots consulten `transition_` i + // `transition_config_` directament a través de `this` — així les + // signatures es queden netes i la funció pública s'orquestra amb + // poques línies. Tres tasques separades: + // - X animada d'una línia (entra per un costat, surt per l'altre). + // - Altura animada del panel translúcid (creix, manté, decreix). + // - Pintat efectiu del panel i d'una línia de text. + [[nodiscard]] auto computeRoomTextX(int width, int phase_enter, int phase_exit, int dur_enter, int dur_exit, bool from_right) const -> int; + [[nodiscard]] auto computeRoomTextPanelHeight(int phase_enter, int phase_hold, int phase_exit, int dur_enter, int dur_exit) const -> int; + void drawRoomTextPanel(int center_y, int panel_height) const; + void drawRoomTextLine(const std::string& text, int x, int y, std::uint8_t fill_color, std::uint8_t outline_color) const; + // Posició inicial dels jugadors derivada de `Map::door_col`. Tots // els jugadors comparteixen Y (centre del corredor exterior superior); // X depèn del nombre actiu: amb un jugador queda centrat sota la