refactor(game): unifica renderitzat dels rètols de transició via TextScreenSpec

This commit is contained in:
JailDesigner committed 2026-05-11 16:32:57 +02:00
1 parent fa27308897
commit 2523661c8f
2 files changed
+127 -154

No files matched your search

+100 -154
View File
@@ -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() {
+27
View File
@@ -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