refactor(player): config a membres d'instancia (P1/P2 YAMLs independents)
This commit is contained in:
1 parent
8eba52353b
commit
2aef925286
4 files changed
+516
-591
No files matched your search
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user