270 lines
13 KiB
C++
270 lines
13 KiB
C++
#pragma once
|
|
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/scene/scene.hpp"
|
|
#include "core/ui/ui_config.hpp"
|
|
#include "game/debug_config.hpp"
|
|
#include "game/entities/entity_manager.hpp"
|
|
#include "game/map/paint_layer.hpp"
|
|
#include "game/map/tilemap.hpp"
|
|
#include "game/scenes/room_fsm.hpp"
|
|
|
|
#ifdef _DEBUG
|
|
#include "game/editor/editor.hpp"
|
|
#endif
|
|
|
|
class Hud;
|
|
class Player;
|
|
class Text;
|
|
|
|
class GameScene : public Scene {
|
|
public:
|
|
// Conjunt de rutes que defineixen una piràmide concreta (mapa + tilemap
|
|
// + sprites dels jugadors + música). Cada piràmide serà un Config distint;
|
|
// `defaultConfig()` retorna la primera (Defaults::Pyramid). `starting_players`
|
|
// controla quins jugadors instancia el ctor: només P1, només P2 o els dos.
|
|
struct Config {
|
|
std::string map_yaml;
|
|
std::string player_sprite;
|
|
std::string player_yaml;
|
|
std::string player2_sprite;
|
|
std::string player2_yaml;
|
|
std::string music_track;
|
|
DebugConfig::StartingPlayers starting_players{DebugConfig::StartingPlayers::BOTH};
|
|
};
|
|
|
|
[[nodiscard]] static auto defaultConfig() -> Config;
|
|
|
|
explicit GameScene(Config config = defaultConfig());
|
|
~GameScene() override;
|
|
|
|
void update(int delta_ms) override;
|
|
void render() override;
|
|
|
|
private:
|
|
// Punt centralitzat per canviar d'estat de sala. Imprimeix la
|
|
// transició i reinicialitza els acumuladors de la `RoomFsm::Transition`.
|
|
// Cap estat s'ha de canviar fora d'aquí.
|
|
void setState(RoomFsm::State new_state);
|
|
|
|
// Avança la transició activa via `RoomFsm::advance`. S'executa cada
|
|
// frame amb delta real (no escalat per la pausa) perquè la transició
|
|
// viu en temps de UI. Si l'estat retornat és diferent de l'actual,
|
|
// crida `setState` (que reseteja la `Transition`).
|
|
void updateRoomTransition(int delta_ms);
|
|
|
|
// Consumeix events d'aquest frame (F11 del KeyConfig "pause" o botó
|
|
// START del gamepad primari) per disparar les transicions de pausa.
|
|
// No alterna un flag binari: inicia (o inverteix mid-flight) un
|
|
// "dial" de temps que frena el món amb easing.
|
|
void handlePauseInput();
|
|
|
|
// Avança la transició activa, recalcula `pause_.scale` i aplica el
|
|
// fade de volum de música. S'executa cada frame amb el delta real
|
|
// (no escalat) perquè la transició viu en temps real.
|
|
void updatePauseTransition(int delta_ms);
|
|
|
|
// Spawna un Dust transitori centrat a (`feet_x_center`, `feet_y`).
|
|
// Cridat per la subscripció del bus a `player_jumped` /
|
|
// `player_landed` per pintar el polvet de despegue / aterratge.
|
|
// `is_landing` selecciona la variant de l'animació al `Dust`.
|
|
void spawnDust(float feet_x_center, float feet_y, bool is_landing);
|
|
|
|
// Spawna un `WhipAttack` a partir del payload publicat al bus quan
|
|
// un Player entra a `ATTACKING`. Llig la plantilla cacheada del YAML
|
|
// i delega la mida + surface al `WhipAttack::configure()` segons
|
|
// direcció.
|
|
void spawnWhip(const struct WhipSpawnPayload& payload);
|
|
|
|
Config config_;
|
|
Tilemap tilemap_;
|
|
|
|
// Gestor d'entitats del nivell (sarcòfags, porta, momia, antorxa,
|
|
// etc.). Viu aquí perquè la seua vida coincideix amb la de la escena;
|
|
// cada entitat del nivell es carrega al ctor via loadFromLevel.
|
|
EntityManager entity_manager_;
|
|
|
|
// Vector des del principi: AEE suportarà multijugador local; cada Player
|
|
// té els seus propis Controls i entra al vector amb una crida extra.
|
|
// Sempre conté **dos** entrades (P1 a [0], P2 a [1]) tant en mode BOTH
|
|
// com en single-player: l'slot inactiu es queda en estat DISABLED de
|
|
// manera permanent, deixant el camí preparat per al "join in progress"
|
|
// futur (el segon jugador podrà entrar a meitat de partida sense
|
|
// recrear estructures). Veure `players_starts_active_`.
|
|
std::vector<std::unique_ptr<Player>> players_;
|
|
|
|
// Quins slots juguen aquesta partida segons `config_.starting_players`.
|
|
// Index = id del jugador (0 = P1, 1 = P2). Determina a qui aplica el
|
|
// spawn aeri al final de la fase 0 d'INTRO; un slot amb `false` es
|
|
// queda DISABLED tot el cicle de sala.
|
|
std::array<bool, 2> players_starts_active_{false, false};
|
|
|
|
// Mil·lisegons des de l'entrada en ROOM_INTRO, per disparar la
|
|
// cinemàtica de spawn aeri quan s'arriba al delay corresponent al
|
|
// slot. Es reseteja a `setState(ROOM_INTRO)`. Cada slot té el seu
|
|
// propi flag de "ja he caigut" — així podem caure en moments
|
|
// distints en mode BOTH (`player_spawn_delay_ms` per a P1 i
|
|
// `player2_spawn_delay_ms` per a P2).
|
|
int room_intro_elapsed_ms_{0};
|
|
std::array<bool, 2> players_spawned_in_intro_{false, false};
|
|
|
|
// One-shot del so del rètol d'INTRO (`text_sound` a game.yaml).
|
|
// Es disparen quan la FSM passa a phase 2 (text_enter); el flag
|
|
// evita repeticions si phase 2 s'estén multi-frame. Es reseteja a
|
|
// `enterRoomIntro` / `setState(ROOM_INTRO)`. Si la clau del so és
|
|
// buida o el cache no l'ha carregat, queda inutilitzat.
|
|
bool intro_text_sound_played_{false};
|
|
|
|
// Cinemàtica de spawn d'un slot concret. Posa el slot indicat en
|
|
// AIRBORNE des d'una altura fora de pantalla. Cridat per
|
|
// `updateRoomTransition` quan `room_intro_elapsed_ms_` supera el
|
|
// delay específic de l'slot. Idempotent (consulta el flag corresponent
|
|
// a `players_spawned_in_intro_`). També és el punt al qual es
|
|
// connectarà el respawn post-mort en el futur.
|
|
void triggerPlayerSpawn(std::size_t slot_index);
|
|
|
|
// Vista plana de punters crus — la fa servir Player com a llista
|
|
// d'obstacles mòbils a la callback de col·lisió. Es construeix un sol
|
|
// cop al ctor (els punters són estables perquè `players_` no es
|
|
// redimensiona dins de la vida de l'escena).
|
|
std::vector<Player*> players_view_;
|
|
|
|
// Factor multiplicatiu del push inter-jugador. Carregat de la clau
|
|
// `push_factor` del YAML del mapa; 0.30 per defecte (veure
|
|
// `Defaults::Player::PUSH_FACTOR`). Constant dins de la vida de
|
|
// l'escena — una piràmide, una física.
|
|
float push_factor_{0.30F};
|
|
|
|
// Capa de "pintat" del terra. Escolta `on_tile_entered` al bus,
|
|
// filtra pintabilitat (sòl transitable) i re-publica `tile_painted`
|
|
// per als sarcòfags. `unique_ptr` per controlar l'ordre de
|
|
// construcció/destrucció: es crea al ctor un cop `entity_manager_` i
|
|
// `tilemap_` estan llestos, i es destrueix abans que `entity_manager_`
|
|
// (on viu el bus al qual està subscrit). Declarat després d'aquests
|
|
// dos membres per garantir l'ordre invers a la destrucció.
|
|
std::unique_ptr<PaintLayer> paint_layer_;
|
|
|
|
// Marcador de la franja superior (24 px). `unique_ptr` per controlar
|
|
// l'ordre de construcció/destrucció igual que `paint_layer_`: es crea
|
|
// al ctor un cop `entity_manager_` ja té el bus, i es destrueix abans
|
|
// que `entity_manager_` (on viu el bus al qual està subscrit).
|
|
// Declarat després d'`entity_manager_` per garantir l'ordre invers a
|
|
// la destrucció.
|
|
std::unique_ptr<Hud> hud_;
|
|
|
|
// Pausa de gameplay com a "dial" de temps. En lloc d'un interruptor,
|
|
// `pause_.scale` va de 1 (món corrent) a 0 (món congelat) amb una
|
|
// corba d'easing durant ENTERING, i al revés durant EXITING. El
|
|
// `delta_ms` del món (players + EntityManager) es multiplica per
|
|
// aquesta escala; HUD, Notifier i GlobalInputs reben sempre el
|
|
// delta real. Les inversions mid-flight capturen la `scale` actual
|
|
// com a `from_scale` i escalen la durada a la distància restant per
|
|
// mantenir la sensació de velocitat. La font és la mateixa que
|
|
// abans — ja cachejada al Director.
|
|
std::shared_ptr<Text> pause_font_;
|
|
|
|
// Font del rètol de transició de sala (ROOM_INTRO i ROOM_OUTRO).
|
|
// Distinta de `pause_font_` perquè el rètol de fase té estètica
|
|
// pròpia (CaesarDressing per defecte, configurable via
|
|
// `data/ui.yaml > fonts.room_text`). La pausa segueix amb la
|
|
// placeholder.
|
|
std::shared_ptr<Text> room_text_font_;
|
|
|
|
enum class PauseState : std::uint8_t {
|
|
IDLE, // Joc corrent; cap transició en marxa. scale == 1.
|
|
ENTERING, // Frenant cap a la pausa. scale decreix a 0.
|
|
PAUSED, // Món congelat. scale == 0.
|
|
EXITING, // Accelerant de tornada. scale creix a 1.
|
|
};
|
|
|
|
struct PauseTransition {
|
|
PauseState state{PauseState::IDLE};
|
|
float scale{1.0F}; // 0 = parat, 1 = corrent.
|
|
float from_scale{1.0F}; // Valor de `scale` al començar la transició.
|
|
float to_scale{1.0F}; // Valor objectiu (0.0 o 1.0).
|
|
int elapsed_ms{0}; // Temps transcorregut dins la transició.
|
|
int duration_ms{0}; // Durada total (escalada a la distància).
|
|
UiConfig::CurveFn curve{nullptr}; // Corba activa.
|
|
} pause_;
|
|
|
|
// Configuració de la FSM de sala (intro/outro). Carregada al ctor des
|
|
// de `data/scenes/game.yaml`; valors per defecte si manca el YAML.
|
|
RoomFsm::Config transition_config_{};
|
|
|
|
// Estat actual de la FSM de sala. S'inicialitza a ROOM_INTRO al ctor
|
|
// perquè cada vegada que entres al joc passes per la presentació de
|
|
// la sala. La transició a PLAYING l'orquestra `updateRoomTransition`.
|
|
RoomFsm::State state_{RoomFsm::State::ROOM_INTRO};
|
|
|
|
// Sub-fase i temps acumulat dins de l'estat actual. `setState` el
|
|
// reseteja a {0, 0}.
|
|
RoomFsm::Transition transition_{};
|
|
|
|
// Píxels del fade negre i del text de la transició de sala.
|
|
// Cridats des de `render()` entre el bloc d'actors i el HUD —
|
|
// així marcador, notificacions i service menu queden per damunt.
|
|
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
|
|
// porta; amb dos, separats ±tile_size respecte al centre. Estructura
|
|
// exposada perquè els tests puguen verificar-la sense entrar al ctor
|
|
// sencer.
|
|
struct PlayerSpawn {
|
|
float p1_x{0.0F};
|
|
float p2_x{0.0F};
|
|
float y{0.0F};
|
|
};
|
|
[[nodiscard]] static auto computeSpawn() -> PlayerSpawn;
|
|
|
|
// Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria,
|
|
// porta tancada), entitats (sarcs/porta/momies des de zero), neteja
|
|
// el PaintLayer i deixa els jugadors a DISABLED esperant la cinemàtica
|
|
// d'spawn aeri del nou cicle d'INTRO. També actualitza
|
|
// `config_.map_yaml` perquè futurs consumidors (l'Editor en guardar,
|
|
// logs, etc.) sàpiguen quina sala està viva.
|
|
void loadRoom(const std::string& yaml_path);
|
|
|
|
// Demana al LevelManager el següent nivell i el carrega via
|
|
// `loadRoom`, deixant la FSM al començament d'un nou cicle d'INTRO.
|
|
// Si ja era a l'última entrada, fa loop a la primera (política
|
|
// temporal mentre no hi haja pantalla de fi de joc). Cridat des de
|
|
// `updateRoomTransition` quan l'OUTRO arriba a la fase final.
|
|
void advanceToNextRoom();
|
|
|
|
// Inicia (o reinicia) el cicle d'entrada de sala: posa l'estat a
|
|
// ROOM_INTRO i reseteja `transition_`, `room_intro_elapsed_ms_` i el
|
|
// flag d'spawn aeri. Idempotent quan ja s'estava a INTRO (els
|
|
// timers tornen a començar des de zero — útil per als salts debug
|
|
// que recarreguen la sala estant ja en INTRO).
|
|
void enterRoomIntro();
|
|
|
|
#ifdef _DEBUG
|
|
// Editor modal del tilemap. Mútuament excloent amb la pausa: si la
|
|
// pausa no està IDLE la tecla de toggle de l'editor s'ignora, i
|
|
// viceversa. Quan `editor_.active()` és true, saltem entitats i
|
|
// jugador (update i render) i deixem el tilemap visible perquè es
|
|
// pugui editar.
|
|
Editor editor_;
|
|
#endif
|
|
};
|