refactor(player): config a membres d'instancia (P1/P2 YAMLs independents)

This commit is contained in:
JailDesigner committed 2026-05-13 21:40:02 +02:00
1 parent 8eba52353b
commit 2aef925286
4 files changed
+516 -591

No files matched your search

+181 -211
View File
@@ -55,6 +55,15 @@ class Player : public AnimatedSprite {
HOLE, // caiguda per agujero — animació `death_hole`
};
// Direcció cardinal completa. Pública perquè el `WhipAttack` la passa a
// `enterHurt` quan colpeja un altre jugador (friendly fire). Declarada
// aquí (a sobre de `Config` perquè `TurnConfig` l'utilitza).
enum class Cardinal : std::uint8_t { NONE,
NORTH,
SOUTH,
EAST,
WEST };
enum class State : std::uint8_t {
GROUNDED,
AIRBORNE,
@@ -92,80 +101,172 @@ class Player : public AnimatedSprite {
DYING,
};
// Geometria carregada des del YAML del player (p.ex. `data/entities/player/sam.yaml`).
// `sprite_*` són
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
// 12 píxels superiors són el cap i els 12 inferiors la base).
// La caixa de col·lisió és un quadrat `collision_size` concèntric amb el
// quadrat inferior del sprite (`base_size`). Els offsets els calculem
// al load a partir d'aquests tres valors; el mapa de col·lisions i el
// de dibuix són independents (el sprite pot sobresortir sense afectar).
inline static int sprite_w = 0;
inline static int sprite_h = 0;
inline static int base_size = 0;
inline static int collision_size = 0;
inline static int collision_padding = 0;
inline static int collision_x_offset = 0;
inline static int collision_y_offset = 0;
// Configuració per-instància del Player. Conté tot el que abans vivia
// com a `inline static` o `static *_config`: així cada jugador (P1=sam,
// P2=morcus) pot tenir paràmetres diferents llegits del seu YAML.
// GameScene la carrega amb `Config::loadFromYaml` abans d'instanciar
// cada Player i la passa al ctor (mou-la). Per refrescar-la entre
// sales s'utilitza `Player::setConfig`.
//
// Els sub-structs (`InertiaConfig`, `TurnConfig`, ...) viuen dins de
// `Config` per agrupar-los visualment amb la resta del config — no hi
// ha cap raó tècnica per haver-los a fora.
struct Config {
// ---- Geometria del sprite i col·lisió --------------------------
// `sprite_*` són la mida del frame (sprite 12×24 amb perspectiva
// falsa). La caixa de col·lisió és un quadrat `collision_size`
// concèntric amb el quadrat inferior del sprite (`base_size`).
// Els offsets es calculen al load.
int sprite_w{0};
int sprite_h{0};
int base_size{0};
int collision_size{0};
int collision_padding{0};
int collision_x_offset{0};
int collision_y_offset{0};
// Caixa de "contacte" més petita que la caixa de col·lisió: s'usa per
// a les interaccions actor-actor (player↔player, player↔momia/trampa),
// on volem que el contacte sigui precís i no dispari a "distància"
// de la vora del sprite. La caixa de col·lisió gran continua usant-se
// per xocar amb murs i sarcòfags (per mantenir el carril del
// passadís). Sense `hit_size` al YAML, s'iguala a `collision_size` i
// el comportament és idèntic a l'anterior.
inline static int hit_size = 0;
inline static int hit_x_offset = 0;
inline static int hit_y_offset = 0;
// Caixa de contacte actor-actor (més petita que la col·lisió).
int hit_size{0};
int hit_x_offset{0};
int hit_y_offset{0};
// Alçada del cos del jugador en píxels (camp `collision.height` del
// YAML, mateixa convenció que entitats com mummy o rolling_rock).
// L'usen els filtres de col·lisió per altura: si un altre actor ens
// creua mentre `jump_z_` és superior a aquest valor, no col·lisionem.
inline static int body_height = 0;
// Alçada del cos en píxels (filtre de col·lisió per altura).
int body_height{0};
// Radi de la llum emesa pel jugador (en píxels del canvas), llegit
// del bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }`
// del YAML. 0 = no emet. Es declara estàtic com la resta de configs
// del jugador (immunity, jump, ...): tots els jugadors del mateix YAML
// comparteixen el valor. `light_flicker` activa la modulació "tipus
// foc" del radi; `light_color_*` són el tint quan la sala usa mode
// additive (en subtractive s'ignora); `light_offset_*` desplaça el
// centre del cercle respecte el centre del sprite (Y creix avall:
// `y: -3` puja la llum 3 px).
inline static int light_radius = 0;
inline static bool light_flicker = false;
inline static std::uint8_t light_color_r = 255;
inline static std::uint8_t light_color_g = 255;
inline static std::uint8_t light_color_b = 255;
inline static int light_offset_x = 0;
inline static int light_offset_y = 0;
// Paràmetres del flicker resolts per al jugador (combinació del
// global + override del YAML del personatge + override de sala).
// Només té efecte si `light_flicker` és true.
inline static Lighting::FlickerConfig flicker_params{};
// ---- Emissió de llum -----------------------------------------
// Llegit del bloc opcional `light:` del YAML.
// `radius` → 0 = no emet.
// `flicker` → activa modulació tipus foc.
// `color` → tint en mode additive (ignorat en subtractive).
// `offset` → desplaçament del centre respecte el sprite (Y avall).
int light_radius{0};
bool light_flicker{false};
std::uint8_t light_color_r{255};
std::uint8_t light_color_g{255};
std::uint8_t light_color_b{255};
int light_offset_x{0};
int light_offset_y{0};
// Paràmetres del flicker resolts (global + override del YAML +
// override de sala). Només té efecte si `light_flicker` és true.
Lighting::FlickerConfig flicker_params{};
// Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor.
static void loadFrom(const std::string& yaml_path);
// ---- Sub-structs de gameplay ---------------------------------
// Vides amb què arrenca un jugador (camp `lives:` del YAML del
// jugador). El consumeix el HUD per inicialitzar el comptador del
// marcador i `Player::resetForJoin` per a re-entrades mid-partida.
[[nodiscard]] static auto getInitialLives() -> int { return death_config.initial_lives; }
// Inèrcia: amb `brake_time_ms == 0` el comportament és instantani.
struct InertiaConfig {
int brake_time_ms{0};
int threshold_ms{0};
};
// Gir de 180°: amb `duration_ms == 0` els canvis de direcció són
// instantanis. `Cardinal` ha d'estar visible — declarada a sobre.
struct TurnConfig {
int duration_ms{0};
Cardinal w_to_e{Cardinal::SOUTH};
Cardinal e_to_w{Cardinal::NORTH};
Cardinal n_to_s{Cardinal::EAST};
Cardinal s_to_n{Cardinal::WEST};
};
// Salt: amb `impulse_px_per_s == 0` el botó no té efecte.
struct JumpConfig {
float impulse_px_per_s{0.0F};
float gravity_px_per_s2{0.0F};
int landing_stun_ms{0};
std::string sound_key;
std::string land_sound_key;
};
// HURT (estat post-impacte). Amb `initial_speed_px_per_s == 0`
// entrada/sortida instantànies (sense moviment).
struct HurtConfig {
float initial_speed_px_per_s{0.0F};
int brake_time_ms{0};
std::string sound_key;
};
// Látigo: hitbox `range_tiles`, swing `swing_ms`, cooldown
// `cooldown_ms`. Per ara `damage` només es llegeix.
struct WhipConfig {
float range_tiles{0.0F};
int swing_ms{0};
int cooldown_ms{0};
int damage{0};
std::string sound_key;
};
// Mort/respawn: `respawn_delay_ms` entre l'animació i la
// reaparició; `initial_lives` és el comptador d'intents.
struct DeathConfig {
int respawn_delay_ms{0};
int initial_lives{3};
};
// Immunitat post-spawn. `duration_ms == 0` desactiva la mecànica.
// `palette_key` (basename del .pal) és la paleta plana del blink.
struct ImmunityConfig {
float duration_ms{0.0F};
int blink_period_ms{0};
std::string palette_key;
};
InertiaConfig inertia{};
TurnConfig turn{};
JumpConfig jump{};
HurtConfig hurt{};
WhipConfig whip{};
DeathConfig death{};
ImmunityConfig immunity{};
// Llig tot el YAML del personatge i retorna un `Config` poblat.
// Substitueix l'antic `Player::loadFrom` que mutava estàtics.
// Llança si manca el bloc `collision:` (obligatori).
[[nodiscard]] static auto loadFromYaml(const std::string& yaml_path) -> Config;
// Aplica l'override de sala (bloc `lighting.emitters.player` del
// YAML del mapa) sobre els camps de llum d'aquesta Config. Es
// crida després de `loadFromYaml` i abans de passar la Config al
// ctor de Player (o a `setConfig` en transicions de sala).
void applyRoomLightOverride();
};
Player(Config cfg, const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
// Reemplaça el `Config` actual del Player. Cridat per GameScene al
// canvi de sala per re-aplicar valors base + override de la sala nova.
// No toca cap estat de joc (lives, posició, FSM): només la config.
void setConfig(Config cfg);
// Accés a la config viva (lectura). El callsites que abans accedien
// a estàtics fan servir `getters d'instància` (sota); aquest és per
// codi que vol l'objecte sencer.
[[nodiscard]] auto getConfig() const -> const Config& { return cfg_; }
// Vides amb què arrenca un jugador (camp `lives:` del YAML). El
// consumeix el HUD per inicialitzar el marcador i `resetForJoin`
// per a re-entrades mid-partida.
[[nodiscard]] auto getInitialLives() const -> int { return cfg_.death.initial_lives; }
// Geometria del cos — instance getters dels camps de `cfg_` que
// alguns callsites externs (game_scene) consulten directament.
[[nodiscard]] auto getBodyHeight() const -> int { return cfg_.body_height; }
[[nodiscard]] auto getSpriteW() const -> int { return cfg_.sprite_w; }
[[nodiscard]] auto getSpriteH() const -> int { return cfg_.sprite_h; }
[[nodiscard]] auto getCollisionSize() const -> int { return cfg_.collision_size; }
[[nodiscard]] auto getCollisionYOffset() const -> int { return cfg_.collision_y_offset; }
// Emissió de llum (mateix paper que `Entity::getLightRadius` per a
// les entitats). El centre puja amb el salt: durant `jump_z_>0` el
// sprite es dibuixa amb `pos_.y - jump_z_`, i la llum ha de seguir-lo
// per a que el cercle "salti" amb el jugador, no es quedi al sòl.
[[nodiscard]] static auto getLightRadius() -> int { return light_radius; }
[[nodiscard]] static auto getLightFlicker() -> bool { return light_flicker; }
[[nodiscard]] static auto getLightColorR() -> std::uint8_t { return light_color_r; }
[[nodiscard]] static auto getLightColorG() -> std::uint8_t { return light_color_g; }
[[nodiscard]] static auto getLightColorB() -> std::uint8_t { return light_color_b; }
[[nodiscard]] static auto getLightOffsetX() -> int { return light_offset_x; }
[[nodiscard]] static auto getLightOffsetY() -> int { return light_offset_y; }
[[nodiscard]] static auto getFlickerParams() -> const Lighting::FlickerConfig& { return flicker_params; }
[[nodiscard]] auto getLightRadius() const -> int { return cfg_.light_radius; }
[[nodiscard]] auto getLightFlicker() const -> bool { return cfg_.light_flicker; }
[[nodiscard]] auto getLightColorR() const -> std::uint8_t { return cfg_.light_color_r; }
[[nodiscard]] auto getLightColorG() const -> std::uint8_t { return cfg_.light_color_g; }
[[nodiscard]] auto getLightColorB() const -> std::uint8_t { return cfg_.light_color_b; }
[[nodiscard]] auto getLightOffsetX() const -> int { return cfg_.light_offset_x; }
[[nodiscard]] auto getLightOffsetY() const -> int { return cfg_.light_offset_y; }
[[nodiscard]] auto getFlickerParams() const -> const Lighting::FlickerConfig& { return cfg_.flicker_params; }
// Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador
// local) amb un offset gran per evitar col·lisions amb el contador
// d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases
@@ -173,8 +274,6 @@ class Player : public AnimatedSprite {
[[nodiscard]] auto getLightSeed() const -> int { return id_ + 1000; }
[[nodiscard]] auto getLightCenter() const -> SDL_FPoint;
Player(const std::string& anim_key, int id, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index = -1);
// Update en una sola crida: manté l'API anterior per als tests i
// escenaris amb un sol jugador. Internament és equivalent a cridar
// `tick(dt)` seguit d'`applyMovement(dt)`.
@@ -275,7 +374,7 @@ class Player : public AnimatedSprite {
// que coincideix amb aquest mètode i s'usa específicament per
// marcar la posició de la sombra a nivell de sòl.
[[nodiscard]] auto getFeetY() const -> float {
return getPosY() + static_cast<float>(collision_y_offset + collision_size);
return getPosY() + static_cast<float>(cfg_.collision_y_offset + cfg_.collision_size);
}
// Alies semàntic: els peus del jugador al sòl. Coincideix amb
@@ -484,7 +583,7 @@ class Player : public AnimatedSprite {
// arbitrària (p. ex. la posició proposada per `tryMove` abans
// d'aplicar-la). Evita recalcular `hit_x_offset`/`hit_y_offset` fora
// de Player.
[[nodiscard]] static auto hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect;
[[nodiscard]] auto hitBoxFromBase(const SDL_FRect& base) const -> SDL_FRect;
// Override del hook de Sprite: `MovingSprite::tryMove` envia aquesta
// caixa al callback de col·lisió. La base del Player és més petita
@@ -568,6 +667,11 @@ class Player : public AnimatedSprite {
// efecte immediat sense haver de tornar a entrar a la GameScene.
void refreshControlsFromOptions();
// Tot el config del personatge (geometria, llum, jump, hurt, whip,
// death, immunity, turn, inertia). Llegit del YAML per instància
// (P1 i P2 poden tenir valors diferents) i mogut al ctor.
Config cfg_;
int id_{-1};
Controls controls_;
@@ -639,26 +743,6 @@ class Player : public AnimatedSprite {
static auto facingSuffix(Facing facing) -> const char*;
static auto statePrefix(State state, bool moving, bool in_attack_swing) -> const char*;
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
// Pública perquè el `WhipAttack` la passa a `enterHurt` quan colpeja
// un altre jugador (friendly fire).
public:
enum class Cardinal : std::uint8_t { NONE,
NORTH,
SOUTH,
EAST,
WEST };
private:
// Configuració d'inèrcia llegida del bloc `inertia:` del YAML. Amb
// `brake_time_ms == 0` (o bloc absent) la inèrcia està desactivada i
// el comportament és instantani: soltar input para el jugador en sec.
struct InertiaConfig {
int brake_time_ms{0}; // Temps en ms perquè la velocitat caiga de ±SPEED a 0.
int threshold_ms{0}; // Temps mínim en moviment continu per activar la inèrcia.
};
static InertiaConfig inertia_config;
// Integra un eix. Si hi ha input en aquest eix, la velocitat física
// salta a aquest valor (acceleració instantània). Si no, i hi ha
// input en l'altre eix (`any_input == true`), la velocitat d'aquest
@@ -666,127 +750,7 @@ class Player : public AnimatedSprite {
// soltant l'input — no volem que arrossegue en diagonal. Només quan
// l'usuari deixa anar tot l'input (`any_input == false`) s'activa la
// inèrcia de frenada (si `motion_time_ms` ha passat el llindar).
static void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input);
// Configuració del gir de 180°, llegida del bloc `turn:` del YAML del
// player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica
// queda desactivada i els canvis de direcció són instantanis.
struct TurnConfig {
int duration_ms{0};
Cardinal w_to_e{Cardinal::SOUTH};
Cardinal e_to_w{Cardinal::NORTH};
Cardinal n_to_s{Cardinal::EAST};
Cardinal s_to_n{Cardinal::WEST};
};
// Definida out-of-class al .cpp: dins de la classe, els initializers
// per defecte de TurnConfig encara no són visibles per al compilador.
static TurnConfig turn_config;
// Configuració del salt llegida del bloc `jump:` del YAML del player.
// Si `impulse_px_per_s == 0` (o el bloc no existeix) la mecànica del
// salt queda desactivada — el botó no té efecte.
struct JumpConfig {
float impulse_px_per_s{0.0F}; // Velocitat vertical inicial.
float gravity_px_per_s2{0.0F}; // Acceleració descendent constant.
int landing_stun_ms{0}; // Stun en aterrar (no es mou ni salta).
std::string sound_key; // Clau al Resource::Cache del so de despegue (buit = silenci).
std::string land_sound_key; // Clau del so d'aterratge (buit = silenci).
};
static JumpConfig jump_config;
// Llig el bloc `collision:` del YAML i n'omple les caixes (size/base/
// hit) + `body_height`. Llança si el bloc no existeix. Membre estàtic
// per accedir als estàtics privats sense exposar-los.
static void readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path);
// Llig el bloc `jump:` del YAML i n'omple `jump_config`. Si el bloc
// manca, deixa la mecànica del salt desactivada.
static void readJumpBlock(const fkyaml::node& yaml);
// Configuració de l'estat HURT (estat post-impacte). Si
// `initial_speed_px_per_s == 0` el jugador entra i surt
// instantàniament (cap moviment).
struct HurtConfig {
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
// Fitxer .wav reproduït en entrar a HURT (relatiu a `data/audio/sounds/`).
// Mateixa convenció que la resta de sons del jugador: el basename val
// de clau al Resource::Cache i de path. Buit o absent = entrada
// silenciosa.
std::string sound_key;
};
static HurtConfig hurt_config;
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
static void readHurtBlock(const fkyaml::node& yaml);
// Configuració del látigo del jugador llegida del bloc `whip:` del
// YAML. `range_tiles` defineix la longitud de la hitbox abans de
// retallar; `swing_ms` és la durada de l'animació d'atac (des
// d'`enterAttacking` fins a desbloquejar el cooldown); `cooldown_ms`
// afegeix una fase posterior durant la qual el jugador segueix
// bloquejat però la hitbox ja no fa dany. `damage` queda llegit per
// a futur; per ara els objectius reben sempre `enterHurt`.
struct WhipConfig {
float range_tiles{0.0F};
int swing_ms{0};
int cooldown_ms{0};
int damage{0};
// Fitxer .wav reproduït al moment de disparar el swing (relatiu a
// `data/audio/sounds/`). Convenció: el basename val tant de clau al
// Resource::Cache com de path. Buit o absent = swing silenciós.
std::string sound_key;
};
static WhipConfig whip_config;
// Llig el bloc `whip:` del YAML i n'omple `whip_config`. Si manca el
// bloc, els camps queden a 0 i el jugador no pot atacar (el flanc
// d'input es perd silenciosament).
static void readWhipBlock(const fkyaml::node& yaml);
// Configuració de mort/respawn llegida del bloc `death:` del YAML.
// `respawn_delay_ms` és la pausa entre la fi de `death_spinning` i la
// reaparició al spawn; `initial_lives` és el comptador d'intents amb
// què naix cada Player. Si el bloc manca, defaults segurs (0/3).
struct DeathConfig {
int respawn_delay_ms{0};
int initial_lives{3};
};
static DeathConfig death_config;
// Llig el bloc `death:` del YAML i n'omple `death_config`. Si manca,
// `respawn_delay_ms` queda a 0 (el respawn és immediat al final de
// l'animació) i `initial_lives` es manté al default (3).
static void readDeathBlock(const fkyaml::node& yaml);
// Configuració de la immunitat post-spawn llegida del bloc `immunity:`
// del YAML. `duration_ms` és el temps per defecte que dura cada
// immunitat (l'invoca `respawn`). `blink_period_ms` és el mig-cicle del
// parpadeig visual: cada `blink_period_ms` ms, el render alterna
// entre la paleta normal i la paleta `palette_key`. Si `duration_ms`
// és 0 (o el bloc manca) la immunitat queda desactivada i el
// comportament és l'antic.
struct ImmunityConfig {
float duration_ms{0.0F};
int blink_period_ms{0};
// Clau al Resource::Cache de la paleta plana usada al blink (basename
// del .pal a `data/entities/player/`). Buida = no hi ha alternança visual,
// només la finestra lògica d'immunitat (que sense feedback visual
// pot ser confusa, però no és incorrecte).
std::string palette_key;
};
static ImmunityConfig immunity_config;
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
// hi ha `palette`, registra la paleta corresponent al Resource::Cache
// (path: `data/entities/player/<palette_key>.pal`). Mateix patró que els altres
// readers — `loadFrom` el crida al final.
static void readImmunityBlock(const fkyaml::node& yaml);
// Llig el bloc opcional `light: { radius: N, flicker: bool }` del
// YAML i pobla `light_radius` / `light_flicker`. Si manca, els
// valors queden a 0 / false (jugador sense llum).
static void readLightBlock(const fkyaml::node& yaml);
void integrateAxis(float& phys, float input, float delta_time, float motion_time_ms, bool any_input) const;
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
@@ -992,10 +956,16 @@ class Player : public AnimatedSprite {
// Extret de `update` per acotar la complexitat cognitiva (clang-tidy).
void updateFacingAndTurn(float delta_time, float vx, float vy);
static auto cardinalFromInput(float vx, float vy) -> Cardinal;
static auto cardinalFromString(const std::string& s) -> Cardinal;
static auto opposite(Cardinal c) -> Cardinal;
static auto intermediateFor(Cardinal target) -> Cardinal;
auto intermediateFor(Cardinal target) const -> Cardinal;
public:
// Helper pur: tradueix un string YAML ("north", "south", ...) al
// Cardinal corresponent. Públic perquè `Config::loadFromYaml` el crida
// des d'un helper de la TU anònima a player.cpp.
static auto cardinalFromString(const std::string& s) -> Cardinal;
private:
// Bus del nivell (opcional). Propietat externa (EntityManager); el
// Player només en guarda un punter per publicar events.
EventBus* bus_{nullptr};