feat(level-manager): HUD i text d'INTRO segueixen el LevelManager
HUD: la GameScene crida hud_->setPyramidRoom amb els valors del
LevelManager al ctor i a cada loadRoom — el marcador deixa de
mostrar 1/1 fix i s'actualitza en avançar de sala, en saltar amb
[/] i en arrencar amb starting_level del debug.yaml.
INTRO a dues línies: substitueix la clau game.room_intro per
game.room_intro_pyramid ("Piramide {p}") i game.room_intro_room
("Sala {r}") als locale ca/en. RoomFsm::formatRoomTitle(int)
desapareix i el reemplacen formatIntroPyramidLine(int) +
formatIntroRoomLine(int). renderRoomText es refactoritza per
dibuixar les dues línies (piràmide damunt, sala davall) amb
writeDX independents — comparteixen animació ara mateix però el
disseny ja les deixa preparades per ser desacoblades en el futur.
Drive-by: elimina el membre room_index_ de GameScene; les tres
ubicacions que el sincronitzaven amb el LevelManager (ctor,
advanceToNextRoom, salt debug) ja no fan falta — renderRoomText
i el HUD pregunten directament al singleton, evitant qualsevol
mirror local desincronitzable.
Test: T9/T10 actualitzats per a les dues noves formatadores.
This commit is contained in:
1 parent
b18bd4d021
commit
e2f6d326aa
7 files changed
+149
-99
No files matched your search
@@ -239,12 +239,11 @@ auto GameScene::defaultConfig() -> Config {
|
||||
GameScene::GameScene(Config config)
|
||||
: config_(std::move(config)),
|
||||
tilemap_(makeTilemap(config_.map_yaml)) {
|
||||
// Sincronitzem l'identificador de la sala amb el LevelManager (la
|
||||
// mateixa font que ha donat el `map_yaml`). El text d'intro encara
|
||||
// consumeix només `{n}` = número de sala dins de la piràmide; quan
|
||||
// afegim el placeholder de piràmide al locale, ací també caldrà
|
||||
// passar-li `currentPyramid()`.
|
||||
room_index_ = Levels::LevelManager::get().currentRoom();
|
||||
// El text d'INTRO i el HUD demanaran piràmide/sala directament al
|
||||
// LevelManager — `GameScene` ja no manté un mirror local. La
|
||||
// sincronització "una vegada per càrrega de sala" la fa `loadRoom`
|
||||
// (per al HUD) i `renderRoomText` (per al text d'INTRO, llegint el
|
||||
// valor a cada frame de la transició).
|
||||
|
||||
// Convenció: el Resource::Cache indexa per basename del fitxer, perquè
|
||||
// la mateixa clau l'usa el parseador d'animació (YAML > tileSetFile).
|
||||
@@ -415,6 +414,13 @@ GameScene::GameScene(Config config)
|
||||
// només P1). Mentre els events del bus no porten `player_id`, els hits
|
||||
// segueixen aplicant-se a P1 (veure TODO a Hud::onPlayerHit).
|
||||
if (USE_P2) { hud_->activatePlayer(2); }
|
||||
// Sincronització inicial dels comptadors de piràmide/sala del HUD amb
|
||||
// el LevelManager (després de cada `loadRoom` també es refresca; ací
|
||||
// és el primer ajust per a la sala d'arrencada).
|
||||
{
|
||||
const auto& mgr = Levels::LevelManager::get();
|
||||
hud_->setPyramidRoom(mgr.currentPyramid(), mgr.currentRoom());
|
||||
}
|
||||
|
||||
// Música de fons en bucle infinit. El track ja està al Resource::Cache
|
||||
// (preload al Director), així que playMusic només mou el punter cap a
|
||||
@@ -486,6 +492,15 @@ void GameScene::loadRoom(const std::string& yaml_path) {
|
||||
// el primer.
|
||||
for (auto& p : players_) { p->disable(); }
|
||||
|
||||
// Refresca el marcador (piràmide / sala) amb el LevelManager. El
|
||||
// GameScene no manté els valors a part: la lectura sempre és a
|
||||
// través del singleton, així no es desincronitzen mai entre
|
||||
// hotkeys de salt, debug.yaml i creuament natural de porta.
|
||||
if (hud_) {
|
||||
const auto& mgr = Levels::LevelManager::get();
|
||||
hud_->setPyramidRoom(mgr.currentPyramid(), mgr.currentRoom());
|
||||
}
|
||||
|
||||
std::cout << "[ROOM] load " << yaml_path << ": entities reloaded, paint cleared, "
|
||||
<< ACTIVE_COUNT << " active player(s) disabled (awaiting spawn cinematic)\n";
|
||||
}
|
||||
@@ -498,7 +513,6 @@ void GameScene::advanceToNextRoom() {
|
||||
std::cout << "[ROOM] final del levels.yaml — bucle a la primera sala\n";
|
||||
mgr.restart();
|
||||
}
|
||||
room_index_ = mgr.currentRoom();
|
||||
loadRoom(mgr.currentPath());
|
||||
enterRoomIntro();
|
||||
}
|
||||
@@ -615,70 +629,83 @@ void GameScene::renderRoomFade() {
|
||||
void GameScene::renderRoomText() {
|
||||
// Text de la transició de sala. Reusa la font cachejada per a la
|
||||
// pausa (mateix tipus de lletra, mateix índex de color), i fa el
|
||||
// mateix patró esquerra → centre → esquerra que el text de pausa
|
||||
// però amb les durades pròpies de la transició.
|
||||
// mateix patró esquerra → centre → dreta que el text de pausa però
|
||||
// amb les durades pròpies de la transició.
|
||||
//
|
||||
// Mapatge per estat (només els estats amb text actiu hi entren):
|
||||
// ROOM_INTRO: phases 2..4 → enter / hold / exit; text = sala N
|
||||
// ROOM_OUTRO: phases 0..2 → enter / hold / exit; text = "fase completada"
|
||||
// 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.
|
||||
// ROOM_OUTRO: phases 0..2 → enter / hold / exit; una línia
|
||||
// ("fase completada").
|
||||
if (!pause_font_) { return; }
|
||||
|
||||
std::string text;
|
||||
int phase_enter = 0;
|
||||
int phase_exit = 0;
|
||||
int dur_enter = 0;
|
||||
int dur_exit = 0;
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) {
|
||||
text = RoomFsm::formatRoomTitle(room_index_);
|
||||
phase_enter = 2;
|
||||
phase_exit = 4;
|
||||
dur_enter = transition_config_.intro_text_enter_ms;
|
||||
dur_exit = transition_config_.intro_text_exit_ms;
|
||||
} else if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase >= 0 && transition_.phase <= 2) {
|
||||
text = Locale::get().text("game.room_complete");
|
||||
phase_enter = 0;
|
||||
phase_exit = 2;
|
||||
dur_enter = transition_config_.outro_text_enter_ms;
|
||||
dur_exit = transition_config_.outro_text_exit_ms;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr int KERNING = 0;
|
||||
const int W = pause_font_->length(text, KERNING);
|
||||
// El text entra des de l'esquerra (off-screen amb el seu cantó dret
|
||||
// a x=0) i, després del hold, surt cap a la dreta (off-screen amb
|
||||
// el seu cantó esquerre a x=WIDTH). Així el text "travessa" la
|
||||
// pantalla en una sola direcció en compte de tornar enrere.
|
||||
const int OFF_LEFT = -W;
|
||||
const int OFF_RIGHT = GameCanvas::WIDTH;
|
||||
int x = GameCanvas::CENTER_X;
|
||||
|
||||
if (transition_.phase == phase_enter) {
|
||||
const float T = (dur_enter > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_enter), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
|
||||
x = Easing::lerpInt(OFF_LEFT, GameCanvas::CENTER_X, CURVE(T));
|
||||
} else if (transition_.phase == phase_exit) {
|
||||
const float T = (dur_exit > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(dur_exit), 0.0F, 1.0F)
|
||||
: 1.0F;
|
||||
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
|
||||
x = Easing::lerpInt(GameCanvas::CENTER_X, OFF_RIGHT, CURVE(T));
|
||||
}
|
||||
|
||||
constexpr Uint8 OUTLINE_WHITE = 255;
|
||||
constexpr Uint8 STROKE_DIST = 1;
|
||||
const int Y = GameCanvas::CENTER_Y - (pause_font_->getLineHeight() / 2);
|
||||
pause_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG,
|
||||
x,
|
||||
Y,
|
||||
text,
|
||||
KERNING,
|
||||
UiConfig::get().notificationText(),
|
||||
STROKE_DIST,
|
||||
OUTLINE_WHITE);
|
||||
|
||||
// Calcula la X animada d'una línia: entra des de l'esquerra
|
||||
// (off-screen amb el seu cantó dret a x=0), s'atura al CENTER_X
|
||||
// durant el hold i surt cap a la dreta (off-screen amb el cantó
|
||||
// esquerre a x=WIDTH). El càlcul depèn de l'amplada de la línia
|
||||
// — per això la passem com a paràmetre.
|
||||
auto compute_x = [&](int width, int phase_enter, int phase_exit, int dur_enter, int dur_exit) -> int {
|
||||
const int OFF_LEFT = -width;
|
||||
const int OFF_RIGHT = GameCanvas::WIDTH;
|
||||
if (transition_.phase == phase_enter) {
|
||||
const float T = (dur_enter > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(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_LEFT, GameCanvas::CENTER_X, CURVE(T));
|
||||
}
|
||||
if (transition_.phase == phase_exit) {
|
||||
const float T = (dur_exit > 0)
|
||||
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(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_RIGHT, CURVE(T));
|
||||
}
|
||||
return GameCanvas::CENTER_X;
|
||||
};
|
||||
|
||||
auto draw_line = [&](const std::string& text, int x, int y) {
|
||||
pause_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG,
|
||||
x,
|
||||
y,
|
||||
text,
|
||||
KERNING,
|
||||
UiConfig::get().notificationText(),
|
||||
STROKE_DIST,
|
||||
OUTLINE_WHITE);
|
||||
};
|
||||
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) {
|
||||
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 = pause_font_->length(LINE_PYRAMID, KERNING);
|
||||
const int W_ROOM = pause_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;
|
||||
const int X_PYRAMID = compute_x(W_PYRAMID, 2, 4, DUR_ENTER, DUR_EXIT);
|
||||
const int X_ROOM = compute_x(W_ROOM, 2, 4, DUR_ENTER, DUR_EXIT);
|
||||
// Bloc de dues línies centrat al voltant de CENTER_Y: la
|
||||
// piràmide ocupa la línia de damunt, la sala la de davall. Si
|
||||
// un dia el font canvia d'alçada, `getLineHeight` ho reflecteix.
|
||||
const int LINE_HEIGHT = pause_font_->getLineHeight();
|
||||
const int Y_PYRAMID = GameCanvas::CENTER_Y - LINE_HEIGHT;
|
||||
const int Y_ROOM = GameCanvas::CENTER_Y;
|
||||
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) {
|
||||
const std::string TEXT = Locale::get().text("game.room_complete");
|
||||
const int W = pause_font_->length(TEXT, KERNING);
|
||||
const int X = compute_x(W, 0, 2, transition_config_.outro_text_enter_ms, transition_config_.outro_text_exit_ms);
|
||||
const int Y = GameCanvas::CENTER_Y - (pause_font_->getLineHeight() / 2);
|
||||
draw_line(TEXT, X, Y);
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::spawnDust(float feet_x_center, float feet_y, bool is_landing) {
|
||||
@@ -833,7 +860,6 @@ void GameScene::update(int delta_ms) {
|
||||
if (DELTA != 0) {
|
||||
auto& mgr = Levels::LevelManager::get();
|
||||
mgr.setIndex(mgr.index() + DELTA);
|
||||
room_index_ = mgr.currentRoom();
|
||||
loadRoom(mgr.currentPath());
|
||||
enterRoomIntro();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user