diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 35eee74..ffacac8 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -1011,11 +1011,6 @@ void GameScene::renderRoomFade() { } void GameScene::renderRoomText() { - // 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 travessen la @@ -1024,82 +1019,35 @@ void GameScene::renderRoomText() { // 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; - // Colors del rètol via UiConfig (`data/config/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(); - const int PHASE = transition_.phase; const int ELAPSED = transition_.elapsed_ms; if (state_ == RoomFsm::State::ROOM_INTRO && PHASE >= 2 && 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). - drawRoomTextPanel(PLAY_CENTER_Y, - computeRoomTextPanelHeight(PHASE, ELAPSED, 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()); - // 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), + renderTextScreen(TextScreenSpec{ + .line1 = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid()), + .line2 = RoomFsm::formatIntroRoomLine(mgr.currentRoom()), + .phase_enter = 2, + .phase_hold = 3, + .phase_exit = 4, + .dur_enter_ms = transition_config_.intro_text_enter_ms, + .dur_exit_ms = transition_config_.intro_text_exit_ms, + .line_gap_px = transition_config_.intro_text_line_gap_px, + }, PHASE, - ELAPSED, - 2, - 4, - DUR_ENTER, - DUR_EXIT, - /*from_right=*/false); - const int X_ROOM = computeRoomTextX(room_text_font_->length(LINE_ROOM, KERNING), - PHASE, - ELAPSED, - 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; - 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); + ELAPSED); } else if (state_ == RoomFsm::State::ROOM_OUTRO && PHASE >= 0 && PHASE <= 2) { - const int DUR_ENTER = transition_config_.outro_text_enter_ms; - const int DUR_EXIT = transition_config_.outro_text_exit_ms; - drawRoomTextPanel(PLAY_CENTER_Y, - computeRoomTextPanelHeight(PHASE, ELAPSED, 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), + renderTextScreen(TextScreenSpec{ + .line1 = Locale::get().text("game.room_complete_line1"), + .line2 = Locale::get().text("game.room_complete_line2"), + .phase_enter = 0, + .phase_hold = 1, + .phase_exit = 2, + .dur_enter_ms = transition_config_.outro_text_enter_ms, + .dur_exit_ms = transition_config_.outro_text_exit_ms, + .line_gap_px = transition_config_.outro_text_line_gap_px, + }, PHASE, - ELAPSED, - 0, - 2, - DUR_ENTER, - DUR_EXIT, - /*from_right=*/false); - const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING), - PHASE, - ELAPSED, - 0, - 2, - DUR_ENTER, - DUR_EXIT, - /*from_right=*/true); - const int GAP = transition_config_.outro_text_line_gap_px; - 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); + ELAPSED); } } @@ -1179,6 +1127,51 @@ void GameScene::drawRoomTextLine(const std::string& text, int x, int y, std::uin outline_color); } +void GameScene::renderTextScreen(const TextScreenSpec& spec, int phase, int elapsed_ms) const { + // Coreografia comuna als quatre rètols (ROOM_INTRO, ROOM_OUTRO, + // CONTINUE?, GAME OVER): panel translúcid de fons + dues línies + // que entren/surten per costats oposats. Els quatre helpers + // existents (`drawRoomTextPanel`, `computeRoomTextPanelHeight`, + // `computeRoomTextX`, `drawRoomTextLine`) fan la feina pesada; + // aquesta funció només els encadena en el mateix ordre per als + // quatre casos. Una `lineN` buida es salta sense pintar — així + // CONTINUE? amaga la línia del countdown fora del hold sense + // que els callers hagin de duplicar la branca. + if (!room_text_font_) { return; } + + constexpr int KERNING = 0; + const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2; + const Uint8 FILL = UiConfig::get().roomText(); + const Uint8 OUTLINE = UiConfig::get().roomTextOutline(); + const int LINE_HEIGHT = room_text_font_->getLineHeight(); + + drawRoomTextPanel(PLAY_CENTER_Y, + computeRoomTextPanelHeight(phase, elapsed_ms, spec.phase_enter, spec.phase_hold, spec.phase_exit, spec.dur_enter_ms, spec.dur_exit_ms)); + + if (!spec.line1.empty()) { + const int X_1 = computeRoomTextX(room_text_font_->length(spec.line1, KERNING), + phase, + elapsed_ms, + spec.phase_enter, + spec.phase_exit, + spec.dur_enter_ms, + spec.dur_exit_ms, + /*from_right=*/false); + drawRoomTextLine(spec.line1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (spec.line_gap_px / 2), FILL, OUTLINE); + } + if (!spec.line2.empty()) { + const int X_2 = computeRoomTextX(room_text_font_->length(spec.line2, KERNING), + phase, + elapsed_ms, + spec.phase_enter, + spec.phase_exit, + spec.dur_enter_ms, + spec.dur_exit_ms, + /*from_right=*/true); + drawRoomTextLine(spec.line2, X_2, PLAY_CENTER_Y + (spec.line_gap_px / 2), FILL, OUTLINE); + } +} + void GameScene::setGameOverState(GameOverFsm::State new_state) { if (new_state == game_over_state_) { return; } const auto PREV = game_over_state_; @@ -1315,104 +1308,57 @@ void GameScene::emitCountdownTickIfChanged() { } void GameScene::renderGameOverText() { - if (!room_text_font_) { return; } + // Mapatge per estat: + // CONTINUE_PROMPT: phases 0=enter, 1=hold (countdown visible), + // 2=exit. La línia 2 (número) només es pinta + // durant la fase 1 perquè els números no + // travessin la pantalla. + // GAME_OVER: phases 0=hold, 1=exit (sense enter anim — el + // panel ja és amunt del CONTINUE_PROMPT). Phase 2 + // (wait) sense text ni panel. if (game_over_state_ != GameOverFsm::State::CONTINUE_PROMPT && game_over_state_ != GameOverFsm::State::GAME_OVER) { return; } - constexpr int KERNING = 0; - const int PLAY_CENTER_Y = (GameCanvas::HEIGHT + Map::hud_h) / 2; - const Uint8 FILL = UiConfig::get().roomText(); - const Uint8 OUTLINE = UiConfig::get().roomTextOutline(); - const int LINE_HEIGHT = room_text_font_->getLineHeight(); const int PHASE = game_over_transition_.phase; const int ELAPSED = game_over_transition_.elapsed_ms; if (game_over_state_ == GameOverFsm::State::CONTINUE_PROMPT) { - const int DUR_ENTER = game_over_config_.continue_text_enter_ms; - const int DUR_EXIT = game_over_config_.continue_text_exit_ms; - // Phases del CONTINUE_PROMPT: 0=enter, 1=hold, 2=exit. El panel - // segueix la mateixa corba enter/hold/exit que el room text. - drawRoomTextPanel(PLAY_CENTER_Y, - computeRoomTextPanelHeight(PHASE, ELAPSED, 0, 1, 2, DUR_ENTER, DUR_EXIT)); - - const std::string LINE_1 = Locale::get().text("game.continue_prompt"); const int SECONDS = GameOverFsm::countdownSecondsRemaining( game_over_state_, game_over_transition_, game_over_config_); - // Línia 2 només té text durant la fase de hold (countdown). En - // les fases d'enter i exit deixem només el "CONTINUE?" perquè - // l'animació dels números no quedi rara. - const std::string LINE_2 = (PHASE == 1) ? std::to_string(SECONDS) : std::string{}; - - const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING), + renderTextScreen(TextScreenSpec{ + .line1 = Locale::get().text("game.continue_prompt"), + .line2 = (PHASE == 1) ? std::to_string(SECONDS) : std::string{}, + .phase_enter = 0, + .phase_hold = 1, + .phase_exit = 2, + .dur_enter_ms = game_over_config_.continue_text_enter_ms, + .dur_exit_ms = game_over_config_.continue_text_exit_ms, + .line_gap_px = transition_config_.intro_text_line_gap_px, + }, PHASE, - ELAPSED, - 0, - 2, - DUR_ENTER, - DUR_EXIT, - /*from_right=*/false); - const int X_2 = computeRoomTextX(LINE_2.empty() ? 0 : room_text_font_->length(LINE_2, KERNING), - PHASE, - ELAPSED, - 0, - 2, - DUR_ENTER, - DUR_EXIT, - /*from_right=*/true); - const int GAP = transition_config_.intro_text_line_gap_px; - drawRoomTextLine(LINE_1, X_1, PLAY_CENTER_Y - LINE_HEIGHT - (GAP / 2), FILL, OUTLINE); - if (!LINE_2.empty()) { - drawRoomTextLine(LINE_2, X_2, PLAY_CENTER_Y + (GAP / 2), FILL, OUTLINE); - } + ELAPSED); return; } - // GAME_OVER: panel sempre amunt durant phase 0/1, decreix en phase 1 - // (exit). Phase 2 (wait) sense panel ni text — només temps mort - // abans del fade. - const int DUR_HOLD = game_over_config_.game_over_text_hold_ms; - const int DUR_EXIT = game_over_config_.game_over_text_exit_ms; + // GAME_OVER: phase 2 (wait) sense panel ni text — només temps mort + // abans del fade. phase_enter=-1 desactiva l'animació d'entrada + // (el panel ja és amunt heretat del CONTINUE_PROMPT). if (PHASE > 1) { return; } - - // Panel: phase 0 = ple, phase 1 = decreix. Reaprofitem - // computeRoomTextPanelHeight passant un phase_enter "fora de rang" - // (-1) perquè la branca d'enter no s'activi mai. - drawRoomTextPanel(PLAY_CENTER_Y, - computeRoomTextPanelHeight(PHASE, ELAPSED, /*phase_enter=*/-1, - /*phase_hold=*/0, - /*phase_exit=*/1, - /*dur_enter=*/0, - DUR_EXIT)); - - const std::string LINE_1 = Locale::get().text("game.game_over_line1"); - const std::string LINE_2 = Locale::get().text("game.game_over_line2"); - // Phase 0 (hold): text quiet al CENTER_X. Phase 1 (exit): el text - // marxa pels costats com el room outro. Reaprofitem computeRoomTextX - // amb phase_enter=-1 (no enter anim) i phase_exit=1. - const int X_1 = computeRoomTextX(room_text_font_->length(LINE_1, KERNING), + renderTextScreen(TextScreenSpec{ + .line1 = Locale::get().text("game.game_over_line1"), + .line2 = Locale::get().text("game.game_over_line2"), + .phase_enter = -1, + .phase_hold = 0, + .phase_exit = 1, + .dur_enter_ms = 0, + .dur_exit_ms = game_over_config_.game_over_text_exit_ms, + .line_gap_px = transition_config_.outro_text_line_gap_px, + }, PHASE, - ELAPSED, - /*phase_enter=*/-1, - /*phase_exit=*/1, - /*dur_enter=*/0, - DUR_EXIT, - /*from_right=*/false); - const int X_2 = computeRoomTextX(room_text_font_->length(LINE_2, KERNING), - PHASE, - ELAPSED, - /*phase_enter=*/-1, - /*phase_exit=*/1, - /*dur_enter=*/0, - DUR_EXIT, - /*from_right=*/true); - const int GAP = transition_config_.outro_text_line_gap_px; - 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); - - (void)DUR_HOLD; // Reservat per a futurs efectes de hold (pulse, etc.). + ELAPSED); } void GameScene::renderGameOverFade() { diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 2abd872..ab9b496 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -372,6 +372,33 @@ class GameScene : public Scene { 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; + // Especificació declarativa d'un rètol de transició de dues línies + // amb panel translúcid. ROOM_INTRO, ROOM_OUTRO, CONTINUE? i GAME OVER + // comparteixen la mateixa coreografia (panel + dues línies en + // direccions oposades) i només divergeixen en el text, els índexs + // de fase i les durades. `phase_enter = -1` desactiva l'animació + // d'entrada (cas GAME_OVER, que apareix amb el panel ja amunt). + // Una `lineN` buida indica que la línia no s'ha de pintar (cas + // CONTINUE? fora del hold, on la línia 2 mostra el countdown). + struct TextScreenSpec { + std::string line1; + std::string line2; + int phase_enter{0}; + int phase_hold{0}; + int phase_exit{0}; + int dur_enter_ms{0}; + int dur_exit_ms{0}; + int line_gap_px{0}; + }; + + // Orquestrador unificat dels quatre rètols de transició. Pinta el + // panel + dues línies a partir de `spec`, deixant la decisió de + // QUÈ pintar (text, fases, durades) als callers `renderRoomText` + // i `renderGameOverText`. Sense estat propi: la fase i temps + // arriben pels paràmetres perquè la mateixa funció serveix les + // dues sub-FSMs (RoomFsm i GameOverFsm). + void renderTextScreen(const TextScreenSpec& spec, int phase, int elapsed_ms) 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