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

+56 -77
View File
@@ -171,9 +171,10 @@ namespace {
// velocitats horitzontals i els deixem rebotar fins a aterrar
// — els dos cauen en stun, com a càstig per abusar del salt.
if (A_AIR) {
// cppcheck-suppress knownConditionTrueFalse
const auto BODY_H = static_cast<float>(Player::body_height);
// cppcheck-suppress knownConditionTrueFalse
// Usem el body_height d'A (els dos haurien de ser idèntics en
// multijugador local actual; si en algun moment difereixen entre
// P1 i P2, prenem el del primer participant per consistència).
const auto BODY_H = static_cast<float>(a->getBodyHeight());
if (BODY_H <= 0.0F) { return; }
if (std::fabs(a->getJumpZ() - b->getJumpZ()) >= BODY_H) { return; }
// Saltadors estàtics horitzontalment (cas degenerat: tots dos
@@ -232,37 +233,14 @@ namespace {
return out;
}
// Override de sala per a la llum del jugador. Si el YAML de la sala
// declara `lighting.emitters.players`, substitueix els valors carregats
// per `Player::loadFrom`. Extret aquí per no inflar la complexitat
// cognitiva del ctor de GameScene (clang-tidy: cognitive-complexity).
// No s'aplica una "tercera capa" per-instància al jugador perquè no
// neix com a spawn del nivell; el dia d'un ítem "linterna" aquell
// sistema mutarà els mateixos camps en runtime — ortogonal a aquest
// override.
void applyRoomLightOverrideToPlayer() {
const auto* room = Map::getRoomEmitterOverride("player");
if (room == nullptr) { return; }
if (!room->enabled) {
Player::light_radius = 0;
Player::light_flicker = false;
return;
}
if (room->radius.has_value()) { Player::light_radius = *room->radius; }
if (room->flicker.has_value()) { Player::light_flicker = *room->flicker; }
if (room->color.has_value()) {
Player::light_color_r = (*room->color)[0];
Player::light_color_g = (*room->color)[1];
Player::light_color_b = (*room->color)[2];
}
if (room->offset.has_value()) {
Player::light_offset_x = (*room->offset)[0];
Player::light_offset_y = (*room->offset)[1];
}
if (room->flicker_wave_amplitude_ratio.has_value()) { Player::flicker_params.wave_amplitude_ratio = *room->flicker_wave_amplitude_ratio; }
if (room->flicker_wave_frequency_hz.has_value()) { Player::flicker_params.wave_frequency_hz = *room->flicker_wave_frequency_hz; }
if (room->flicker_noise_amplitude_ratio.has_value()) { Player::flicker_params.noise_amplitude_ratio = *room->flicker_noise_amplitude_ratio; }
if (room->flicker_noise_frequency_hz.has_value()) { Player::flicker_params.noise_frequency_hz = *room->flicker_noise_frequency_hz; }
// Carrega la config d'un jugador des del seu YAML i aplica l'override
// de sala (si el bloc `lighting.emitters.player` el reescriu). Helper
// compartit entre ctor i `loadRoom` per fer la mateixa cosa als dos
// moments: arrencada de partida i transició de sala.
auto loadPlayerConfigWithRoomOverride(const std::string& yaml_path) -> Player::Config {
auto cfg = Player::Config::loadFromYaml(yaml_path);
cfg.applyRoomLightOverride();
return cfg;
}
} // namespace
@@ -315,14 +293,14 @@ GameScene::GameScene(Config config)
const std::string ANIM_P1 = load_player_assets(config_.player_sprite, config_.player_yaml);
const std::string ANIM_P2 = load_player_assets(config_.player2_sprite, config_.player2_yaml);
// Geometria de col·lisió del jugador (caixa 16×16, base 12): carregada
// del mateix YAML que l'animació. Ha d'estar feta abans d'instanciar
// Player perquè el spawn consulta `collision_*`. Els membres de Player
// són `static inline`, compartits per tots els jugadors; carregar des
// del YAML de P1 és suficient (els dos personatges comparteixen
// geometria — morcus és un repaletat de sam).
Player::loadFrom(config_.player_yaml);
applyRoomLightOverrideToPlayer();
// Geometria de col·lisió i tota la resta de config del jugador
// (caixa 16×16, base 12, jump, hurt, llum, etc.) carregada per
// instància des del YAML del personatge corresponent. Cada Player
// rep el seu `Config` al ctor (moure'l avall). L'override de sala
// s'aplica aquí abans de moure'l perquè el jugador ja arrenqui
// amb la llum correcta per a la sala actual.
Player::Config cfg_p1 = loadPlayerConfigWithRoomOverride(config_.player_yaml);
Player::Config cfg_p2 = loadPlayerConfigWithRoomOverride(config_.player2_yaml);
// Carrega les entitats del nivell (antorxa, sarcòfags, porta, momia,
// etc.). Requereix que Map::loadFrom ja s'hagi cridat (ho fa
@@ -418,8 +396,9 @@ GameScene::GameScene(Config config)
// Spawn lligat a la porta: tots els jugadors apareixen davant del
// passadís de sortida. `computeSpawn` deriva les coordenades de
// `Map::door_col` perquè canviar la columna de la porta al YAML
// moga automàticament l'spawn — no hi ha cap valor hardcoded.
const auto SPAWN = computeSpawn();
// moga automàticament l'spawn — no hi ha cap valor hardcoded. P1
// dicta el centrament (consistent amb el comportament pre-refactor).
const auto SPAWN = computeSpawn(cfg_p1);
const bool BOTH_ACTIVE = (USE_P1 && USE_P2);
// Índex de tile amb què pinta cada jugador — llegit del YAML del mapa
@@ -445,8 +424,8 @@ GameScene::GameScene(Config config)
if (USE_P1) { p1_x = SPAWN.p1_x + TILE_F; }
if (USE_P2) { p2_x = SPAWN.p2_x - TILE_F; }
}
players_.emplace_back(std::make_unique<Player>(ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
players_.emplace_back(std::make_unique<Player>(ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
players_.emplace_back(std::make_unique<Player>(std::move(cfg_p1), ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
players_.emplace_back(std::make_unique<Player>(std::move(cfg_p2), ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
// Tots els slots arrenquen DISABLED. Els actius els reactivarà el
// callback de fi de música d'INTRO via `completeIntro()` (spawn aeri
@@ -499,10 +478,12 @@ GameScene::GameScene(Config config)
auto hud_font_surface = Resource::Cache::get().getSurface(HUD_FONT_GIF_KEY);
auto hud_font = std::make_shared<Text>(hud_font_surface, UiConfig::get().hudFont().fnt);
hud_font->setDefaultKerning(UiConfig::get().hudFont().kerning);
// Vides inicials del HUD = `initial_lives` del YAML del jugador
// (`Player::loadFrom` ja ha omplert `death_config` abans d'aquí).
// Sense això, el marcador es queda fixe a 3 i el YAML perd efecte.
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, Player::getInitialLives());
// Vides inicials del HUD = `initial_lives` del Config del P1.
// (Cada Player té el seu propi Config post-refactor a per-instance;
// el HUD agafa el de P1 com a referència — historicament era el
// valor compartit per tots els jugadors). Si en algun futur P1 i P2
// arrenquen amb vides diferents, caldria un HUD per slot.
hud_ = std::make_unique<Hud>(entity_manager_.getBus(), hud_font, players_[0]->getInitialLives());
// Activem el bloc HUD per als slots que arranquen en partida. Els
// inactius queden mostrant el prompt "POLSA START / PER JUGAR" via
// `refreshJoinPrompt` cada frame. Tots dos s'haurien de poder
@@ -564,19 +545,19 @@ GameScene::~GameScene() {
}
}
auto GameScene::computeSpawn() -> PlayerSpawn {
auto GameScene::computeSpawn(const Player::Config& cfg) -> PlayerSpawn {
// Centre de la columna del passadís en píxels canvas. La caixa de
// col·lisió del jugador (12 px) cap dins del passadís (16 px) amb 2
// px de marge a banda i banda; alinear el sprite al centre de la
// columna garanteix que la base del jugador i la del passadís
// coincideixin verticalment.
const float DOOR_CENTER_X = static_cast<float>((Map::door_col * Map::tile_size) + Map::tilemap_origin_x) + (static_cast<float>(Map::tile_size) / 2.0F);
const float CENTER_TL_X = DOOR_CENTER_X - (static_cast<float>(Player::sprite_w) / 2.0F);
const float CENTER_TL_X = DOOR_CENTER_X - (static_cast<float>(cfg.sprite_w) / 2.0F);
// Y: centre del corredor exterior superior (mateix valor que abans;
// el corredor és simètric respecte a `playfield_y`).
const float CORRIDOR_CENTER_Y = static_cast<float>(Map::playfield_y) + (static_cast<float>(Map::outer_corridor) / 2.0F);
const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(Player::collision_y_offset) - (static_cast<float>(Player::collision_size) / 2.0F);
const float SPAWN_Y = CORRIDOR_CENTER_Y - static_cast<float>(cfg.collision_y_offset) - (static_cast<float>(cfg.collision_size) / 2.0F);
// Mode BOTH: P1 a -tile_size, P2 a +tile_size respecte al centre.
// Els callsites individuals decideixen si usar aquests valors o si
@@ -594,14 +575,18 @@ void GameScene::loadRoom(const std::string& yaml_path) {
// CLOSED amb visited buit, momies i altres actors a la posició inicial.
config_.map_yaml = yaml_path;
Map::loadFrom(yaml_path);
// Refresquem els statics del Player a partir del YAML i tornem a
// aplicar l'override d'aquesta sala. Sense això, una sala amb
// `players: false` deixa `light_radius=0` i, en transicionar a una
// altra sala que no toqui el camp (o digui `players: true`), els
// jugadors no tornen a emetre — l'override actual és "additiu"
// sobre els statics existents, no un reset.
Player::loadFrom(config_.player_yaml);
applyRoomLightOverrideToPlayer();
// Refresquem la Config de cada jugador llegint el seu YAML i aplicant
// l'override d'aquesta sala. Sense això, una sala amb `players: false`
// deixaria `light_radius=0` al jugador i, en transicionar a una altra
// sala que no toqui el camp (o digui `players: true`), els jugadors
// no tornarien a emetre — l'override és "additiu" sobre el Config
// actual, no un reset, així que cal partir sempre del YAML.
if (!players_.empty() && players_[0] != nullptr) {
players_[0]->setConfig(loadPlayerConfigWithRoomOverride(config_.player_yaml));
}
if (players_.size() >= 2 && players_[1] != nullptr) {
players_[1]->setConfig(loadPlayerConfigWithRoomOverride(config_.player2_yaml));
}
// Tilemap pre-blita els tiles a un buffer ARGB intern al ctor — sense
// reconstruir-lo, la imatge de la sala anterior es queda fixada per a
// sempre. PaintLayer captura `tileset_path_`/`tileset_cols_` per valor
@@ -705,7 +690,7 @@ void GameScene::respawnPlayersInstant() {
// música de sala, sense caiguda aèria). Reusem `computeSpawn` i el
// mateix patró de `triggerPlayerSpawn` però cridem `respawn(x, y)`
// (teleport instantani) en lloc de `spawnFromAir(x, y)`.
const auto SPAWN = computeSpawn();
const auto SPAWN = computeSpawn(players_[0]->getConfig());
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
const float SINGLE_X = SPAWN.p1_x + static_cast<float>(Map::tile_size);
for (std::size_t i = 0; i < players_.size(); ++i) {
@@ -736,7 +721,7 @@ void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
// Recalcula spawn per si door_col canvia entre sales (avui no, però
// la lògica ja queda preparada per a l'itinerari).
const auto SPAWN = computeSpawn();
const auto SPAWN = computeSpawn(players_[0]->getConfig());
const bool BOTH_ACTIVE = (players_active_[0] && players_active_[1]);
// Mode single-player: l'únic actiu queda centrat sota la porta. Els
// valors de SPAWN.p1_x/p2_x estan fets per al cas BOTH (offset ±tile);
@@ -841,7 +826,7 @@ void GameScene::joinPlayer(std::size_t slot_index) {
// porta de sortida. Mai el centre (en single-player la cinemàtica
// inicial sí que centra el slot actiu, però en un join mid-partida
// volem el costat per coherència visual amb la regla 2).
const auto SPAWN = computeSpawn();
const auto SPAWN = computeSpawn(players_[0]->getConfig());
const float X = (slot_index == 0) ? SPAWN.p1_x : SPAWN.p2_x;
players_[slot_index]->setSpawnPoint(X, SPAWN.y);
players_[slot_index]->resetForJoin();
@@ -1055,22 +1040,16 @@ void GameScene::applyLightingPass() {
// serà tocar `Map::light_level` per sales concretes.
std::vector<Lighting::Emitter> emitters;
emitters.reserve(players_.size() + entity_manager_.entityCount());
const int PLAYER_R = Player::getLightRadius();
// Quan el YAML del jugador no declara `light:`, `PLAYER_R` és 0 i
// `Lighting::apply` salta el cercle al filtre intern. No fem un
// gating extern aquí (els jugadors són només 1-2 actors i el cost
// d'afegir-los al vector és menyspreable).
const bool PLAYER_FLICKER = Player::getLightFlicker();
const Lighting::Color PLAYER_COLOR{
.r = Player::getLightColorR(),
.g = Player::getLightColorG(),
.b = Player::getLightColorB(),
};
const Lighting::FlickerConfig PLAYER_FP = Player::getFlickerParams();
// Cada jugador té el seu propi config (post-refactor a per-instance):
// radi, color, flicker_params, etc. poden diferir entre P1 i P2.
// Quan el YAML d'un jugador no declara `light:`, el radi és 0 i
// `Lighting::apply` salta el cercle al filtre intern.
for (const auto& p : players_) {
if (p->isOutOfPlay()) { continue; }
const int R = p->getLightRadius();
const auto C = p->getLightCenter();
emitters.push_back({.cx = C.x, .cy = C.y, .radius = PLAYER_R, .seed = p->getLightSeed(), .flicker = PLAYER_FLICKER, .color = PLAYER_COLOR, .flicker_params = PLAYER_FP});
const Lighting::Color COL{.r = p->getLightColorR(), .g = p->getLightColorG(), .b = p->getLightColorB()};
emitters.push_back({.cx = C.x, .cy = C.y, .radius = R, .seed = p->getLightSeed(), .flicker = p->getLightFlicker(), .color = COL, .flicker_params = p->getFlickerParams()});
}
for (const auto* e : entity_manager_.getEntities()) {
const int R = e->getLightRadius();
+6 -2
View File
@@ -11,6 +11,7 @@
#include "core/ui/ui_config.hpp"
#include "game/debug_config.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp"
#include "game/map/paint_layer.hpp"
#include "game/map/tilemap.hpp"
#include "game/scenes/game_over_fsm.hpp"
@@ -21,7 +22,6 @@
#endif
class Hud;
class Player;
class Text;
class GameScene : public Scene {
@@ -382,7 +382,11 @@ class GameScene : public Scene {
float p2_x{0.0F};
float y{0.0F};
};
[[nodiscard]] static auto computeSpawn() -> PlayerSpawn;
// Calcula la posició de spawn dels jugadors usant la geometria del
// sprite/col·lisió del `Config` passat (típicament la del P1, que
// dicta el centrament — si en algun futur P2 té geometria diferent,
// el seu propi spawn pot recalcular-se amb el seu `cfg`).
[[nodiscard]] static auto computeSpawn(const Player::Config& cfg) -> PlayerSpawn;
// Recarrega la sala apuntada per `yaml_path`: refresca Map (geometria,
// porta tancada), entitats (sarcs/porta/momies des de zero), neteja