From 822bed0748b5629e87fc7c423fb4b260d46294c5 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 18 May 2026 16:28:36 +0200 Subject: [PATCH 1/2] useStlAlgorithm: ranges algorithms + justifica returnDanglingLifetime --- source/core/resources/resource_pack.cpp | 10 +++------- source/game/entities/animation_component.cpp | 15 +++++---------- source/game/entities/concrete/whip_attack.cpp | 6 ++---- source/game/entities/entity_loader.cpp | 5 +++-- source/game/entities/entity_manager.cpp | 4 ++-- 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/source/core/resources/resource_pack.cpp b/source/core/resources/resource_pack.cpp index ef981fc..1ec55f4 100644 --- a/source/core/resources/resource_pack.cpp +++ b/source/core/resources/resource_pack.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "utils/log.hpp" @@ -19,12 +20,7 @@ namespace Resource { auto Pack::calculateChecksum(const std::vector& data) -> std::uint32_t { // djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai // reutilitzem eines de diagnòstic entre projectes. - std::uint32_t checksum = 0x12345678; - for (unsigned char b : data) { - // cppcheck-suppress useStlAlgorithm - checksum = ((checksum << 5) + checksum) + b; - } - return checksum; + return std::accumulate(data.begin(), data.end(), std::uint32_t{0x12345678}, [](std::uint32_t acc, unsigned char b) { return ((acc << 5) + acc) + b; }); } void Pack::xorInPlace(std::vector& data, const std::string& key) { @@ -151,7 +147,7 @@ namespace Resource { return false; } - // cppcheck-suppress useStlAlgorithm + // cppcheck-suppress useStlAlgorithm -- bucle amb early-return en error (addFile fallit) + filtre amb continue: no es pot expressar net amb std::ranges::all_of perquè la lògica combina filtratge i acumulació amb sortida prematura. for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { if (!entry.is_regular_file()) { continue; } diff --git a/source/game/entities/animation_component.cpp b/source/game/entities/animation_component.cpp index 2071595..3ac6b25 100644 --- a/source/game/entities/animation_component.cpp +++ b/source/game/entities/animation_component.cpp @@ -1,6 +1,8 @@ #include "game/entities/animation_component.hpp" +#include #include +#include #include #include @@ -33,8 +35,7 @@ namespace { if (speeds.empty()) { return std::vector(frame_count, K_DEFAULT_FRAME_SECONDS); } - // cppcheck-suppress useStlAlgorithm - for (auto& s : speeds) { s = sanitizeSpeed(s); } + std::ranges::transform(speeds, speeds.begin(), sanitizeSpeed); if (speeds.size() == frame_count) { return speeds; } if (speeds.size() > frame_count) { speeds.resize(frame_count); @@ -50,10 +51,7 @@ namespace { // component vàlid. auto parseFrames(const fkyaml::node& anim) -> std::vector { std::vector frames; - for (const auto& f : anim["frames"]) { - // cppcheck-suppress useStlAlgorithm - frames.push_back(f.get_value()); - } + std::ranges::transform(anim["frames"], std::back_inserter(frames), [](const fkyaml::node& f) { return f.template get_value(); }); if (frames.empty()) { frames.push_back(0); } return frames; } @@ -66,10 +64,7 @@ namespace { std::vector speeds_secs; const auto& fm = anim["frame_ms"]; if (fm.is_sequence()) { - for (const auto& v : fm) { - // cppcheck-suppress useStlAlgorithm - speeds_secs.push_back(static_cast(v.get_value()) / 1000.0F); - } + std::ranges::transform(fm, std::back_inserter(speeds_secs), [](const fkyaml::node& v) { return static_cast(v.template get_value()) / 1000.0F; }); } else { const float SECS = static_cast(fm.get_value()) / 1000.0F; speeds_secs.assign(frame_count, SECS); diff --git a/source/game/entities/concrete/whip_attack.cpp b/source/game/entities/concrete/whip_attack.cpp index e39ecc1..176adfd 100644 --- a/source/game/entities/concrete/whip_attack.cpp +++ b/source/game/entities/concrete/whip_attack.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -41,10 +42,7 @@ namespace { if (!yaml.contains("animations") || !yaml["animations"].contains("swing")) { return; } const auto& sw = yaml["animations"]["swing"]; if (!sw.contains("active_frames")) { return; } - for (const auto& f : sw["active_frames"]) { - // cppcheck-suppress useStlAlgorithm - active_out.push_back(f.get_value()); - } + std::ranges::transform(sw["active_frames"], std::back_inserter(active_out), [](const fkyaml::node& f) { return f.template get_value(); }); } } // namespace diff --git a/source/game/entities/entity_loader.cpp b/source/game/entities/entity_loader.cpp index 934ed25..3b89cd5 100644 --- a/source/game/entities/entity_loader.cpp +++ b/source/game/entities/entity_loader.cpp @@ -1,5 +1,7 @@ #include "game/entities/entity_loader.hpp" +#include +#include #include #include @@ -51,8 +53,7 @@ namespace EntityLoader { const auto& list = root["entities"]; if (!list.is_sequence()) { return spawns; } spawns.reserve(list.size()); - // cppcheck-suppress useStlAlgorithm - for (const auto& item : list) { spawns.push_back(item); } + std::ranges::copy(list, std::back_inserter(spawns)); return spawns; } diff --git a/source/game/entities/entity_manager.cpp b/source/game/entities/entity_manager.cpp index 403bdab..73789a3 100644 --- a/source/game/entities/entity_manager.cpp +++ b/source/game/entities/entity_manager.cpp @@ -87,7 +87,7 @@ auto EntityManager::add(std::unique_ptr entity) -> Entity* { entity->setEntityManager(this); Entity* raw = entity.get(); entities_.push_back(std::move(entity)); - // cppcheck-suppress returnDanglingLifetime + // cppcheck-suppress returnDanglingLifetime -- fals positiu: std::move transfereix ownership del unique_ptr al vector, però l'objecte apuntat per `raw` no es mou ni es destrueix. El punter continua vàlid mentre el vector el posseesca. return raw; } @@ -124,7 +124,7 @@ auto EntityManager::addSarcophagus(std::unique_ptr sarc) -> Sarcoph Sarcophagus* raw = sarc.get(); entities_.push_back(std::move(sarc)); ++total_sarcs_; - // cppcheck-suppress returnDanglingLifetime + // cppcheck-suppress returnDanglingLifetime -- fals positiu: std::move transfereix ownership al vector però l'objecte no es mou. `raw` continua vàlid (vegeu `add` més amunt). return raw; } From 7a859c1e443f47f02fc81c0a06d49943d1ccd259 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 18 May 2026 16:41:15 +0200 Subject: [PATCH 2/2] neteja NOLINT obsolets (audio 14 + player + ajusta justificacions) --- source/core/audio/audio.cpp | 14 -- source/core/audio/jail_audio.cpp | 2 +- source/core/rendering/screenshot.cpp | 2 +- source/game/editor/editor.cpp | 4 +- source/game/entities/entity_factory.hpp | 25 ++- source/game/entities/player.cpp | 279 +++++++++++++----------- source/game/entities/player.hpp | 9 + 7 files changed, 175 insertions(+), 160 deletions(-) diff --git a/source/core/audio/audio.cpp b/source/core/audio/audio.cpp index e54b3c5..461f387 100644 --- a/source/core/audio/audio.cpp +++ b/source/core/audio/audio.cpp @@ -90,7 +90,6 @@ void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossf } // Pausa la música (l'estat el transiciona Engine::pauseMusic) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::pauseMusic() { if (music_enabled_ && getMusicState() == MusicState::PLAYING) { engine_->pauseMusic(); @@ -98,7 +97,6 @@ void Audio::pauseMusic() { } // Continua la música pausada (l'estat el transiciona Engine::resumeMusic) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::resumeMusic() { if (music_enabled_ && getMusicState() == MusicState::PAUSED) { engine_->resumeMusic(); @@ -106,14 +104,12 @@ void Audio::resumeMusic() { } // Atura la música (l'estat el transiciona Engine::stopMusic) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::stopMusic() { if (music_enabled_) { engine_->stopMusic(); } } -// NOLINTNEXTLINE(readability-make-member-function-const) void Audio::setMusicSpeed(float ratio) { if (music_enabled_) { engine_->setMusicSpeed(ratio); @@ -121,7 +117,6 @@ void Audio::setMusicSpeed(float ratio) { } // Reprodueix un so per nom -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::playSound(const std::string& name, Group group) { if (sound_enabled_) { engine_->playSound(AudioResource::getSound(name), 0, static_cast(group)); @@ -129,7 +124,6 @@ void Audio::playSound(const std::string& name, Group group) { } // Reprodueix un so per punter directe -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::playSound(Ja::Sound* sound, Group group) { if (sound_enabled_ && sound != nullptr) { engine_->playSound(sound, 0, static_cast(group)); @@ -141,7 +135,6 @@ void Audio::playSound(Ja::Sound* sound, Group group) { // processa tot el sample amb el ratio des del primer pull del callback. // Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem // la crida al ratio — sense efectes col·laterals. -// NOLINTNEXTLINE(readability-make-member-function-const) void Audio::playSound(const std::string& name, Group group, float speed) { if (!sound_enabled_) { return; } auto* sound = AudioResource::getSound(name); @@ -155,7 +148,6 @@ void Audio::playSound(const std::string& name, Group group, float speed) { // Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no // existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound // sec — l'usuari sent el so encara que la cua no s'apliqui. -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) { if (!sound_enabled_) { return; } auto* sound = AudioResource::getSound(name); @@ -175,7 +167,6 @@ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset // Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix // fallback que playSoundWithEcho. -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_ void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) { if (!sound_enabled_) { return; } auto* sound = AudioResource::getSound(name); @@ -194,7 +185,6 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres } // Atura tots els sons -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::stopAllSounds() { if (sound_enabled_) { engine_->stopChannel(-1); @@ -202,7 +192,6 @@ void Audio::stopAllSounds() { } // Fa una fosa de sortida de la música -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::fadeOutMusic(int milliseconds) { if (music_enabled_ && getMusicState() == MusicState::PLAYING) { engine_->fadeOutMusic(milliseconds); @@ -212,7 +201,6 @@ void Audio::fadeOutMusic(int milliseconds) { // Registra un callback que el motor dispararà quan la pista actual acabi de // drenar (times == 0 + stream buit). S'executa al mateix thread que // Audio::update (render loop); els consumidors no poden fer I/O blocant. -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_ void Audio::setOnMusicEnded(std::function callback) { if (engine_) { engine_->setOnMusicEnded(std::move(callback)); } } @@ -251,13 +239,11 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float { } // Estableix el volum dels sons (float 0.0..1.0) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::setSoundVolume(float sound_volume, Group group) { engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast(group)); } // Estableix el volum de la música (float 0.0..1.0) -// NOLINTNEXTLINE(readability-make-member-function-const) — muta estat via engine_; unique_ptr::operator-> no propaga const void Audio::setMusicVolume(float music_volume) { engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_)); } diff --git a/source/core/audio/jail_audio.cpp b/source/core/audio/jail_audio.cpp index 20e57d8..a80e2d4 100644 --- a/source/core/audio/jail_audio.cpp +++ b/source/core/audio/jail_audio.cpp @@ -15,7 +15,7 @@ // (TU aïllat perquè clang-analyzer no dispari fals positius al nostre codi). #define STB_VORBIS_HEADER_ONLY // clang-format off -// NOLINTNEXTLINE(bugprone-suspicious-include) +// NOLINTNEXTLINE(bugprone-suspicious-include) -- stb_vorbis és single-file: la macro de dalt limita aquest TU a només-declaracions; la implementació viu a external/stb_vorbis_impl.cpp. #include "external/stb_vorbis.c" // clang-format on diff --git a/source/core/rendering/screenshot.cpp b/source/core/rendering/screenshot.cpp index 32242b1..b498457 100644 --- a/source/core/rendering/screenshot.cpp +++ b/source/core/rendering/screenshot.cpp @@ -17,7 +17,7 @@ namespace Screenshot { // Estat de mòdul: ruta base on s'escriu la subcarpeta // `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback // si el Director no ha pogut establir la ruta per plataforma). - std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) + std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) -- estat de mòdul (singleton-like): s'estableix una vegada al startup via setBaseDir() i es llegeix N vegades; encapsulat dins del namespace anònim, no és accessible des de fora. // Construeix la ruta del directori on van les captures: // `/screenshots/`. Si la base és buida, retorna diff --git a/source/game/editor/editor.cpp b/source/game/editor/editor.cpp index 0ba3acd..6875de3 100644 --- a/source/game/editor/editor.cpp +++ b/source/game/editor/editor.cpp @@ -345,9 +345,7 @@ namespace { } // namespace -// cppcheck-suppress constParameterReference -// Tilemap& mutable per simetria amb `toggle()` i per futures -// invalidacions de caches d'edició, encara que `save()` sigui const. +// cppcheck-suppress constParameterReference -- Tilemap& mutable per simetria amb `toggle()` i per futures invalidacions de caches d'edició; cppcheck no pot anticipar que les futures mutacions estan previstes en el contracte. void Editor::exit(Tilemap& tilemap, const EntityManager& entities) { if (!active_) { return; } // Constraint del nivell: cal exactament una porta i com a mínim un diff --git a/source/game/entities/entity_factory.hpp b/source/game/entities/entity_factory.hpp index 3df2a66..753c2a3 100644 --- a/source/game/entities/entity_factory.hpp +++ b/source/game/entities/entity_factory.hpp @@ -53,18 +53,19 @@ class EntityFactory { // IMPORTANT: perquè el registrador es cridi, el `.cpp` que el conté ha // d'estar enllaçat directament a l'executable (no dins una STATIC lib // sense `--whole-archive`). Tots els `concrete/*.cpp` entren a APP_SOURCES. -#define REGISTER_ENTITY_AS(NAME, CLASS, TAG) \ - namespace { \ - struct TAG##_Registrar { \ - TAG##_Registrar() { \ - EntityFactory::instance().registerCreator( \ - NAME, \ - [](const fkyaml::node& yaml) -> std::unique_ptr { \ - return std::make_unique(yaml); \ - }); \ - } \ - }; \ - const TAG##_Registrar g_##TAG##_registrar; /* NOLINT(cert-err58-cpp) */ \ +#define REGISTER_ENTITY_AS(NAME, CLASS, TAG) \ + namespace { \ + struct TAG##_Registrar { \ + TAG##_Registrar() { \ + EntityFactory::instance().registerCreator( \ + NAME, \ + [](const fkyaml::node& yaml) -> std::unique_ptr { \ + return std::make_unique(yaml); \ + }); \ + } \ + }; \ + /* NOLINTNEXTLINE(cert-err58-cpp) -- inicialització estàtica intencionada: el constructor registra el creator a l'EntityFactory abans de main(); el patró és l'únic mecanisme C++ que permet auto-registre per tipus sense modificar la factory. */ \ + const TAG##_Registrar g_##TAG##_registrar; \ } #define REGISTER_ENTITY(NAME, CLASS) REGISTER_ENTITY_AS(NAME, CLASS, CLASS) diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 69e6c35..663c834 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -513,143 +513,115 @@ void Player::integrateAxis(float& phys, float input, float delta_time, float mot } } -// Pipeline d'estats i input separats; extracció a helpers empitjoraria la -// llegibilitat. La complexitat puja 1 per la guarda EXITED. -// NOLINTNEXTLINE(readability-function-cognitive-complexity) -void Player::tick(float delta_time) { - // EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap - // física, cap bus event. Continua sent al vector de jugadors per - // simplicitat (no s'esborra) però queda completament inert fins - // que GameScene el reactiva (respawn / spawnFromAir). - if (isOutOfPlay()) { return; } +// DYING: animació `death_spinning` + pausa de respawn. Ignorem tot +// l'input (ni moviment, ni salt, ni atac). El moviment queda +// congelat al lloc de la mort; `applyMovement` avança l'animació i +// detecta la fi per disparar el respawn. Mantenim els flancs de +// botó actualitzats perquè una tecla sostinguda no es comporte com +// a flanc nou en sortir del cicle. +void Player::tickDying() { + (void)consumeJumpEdge(); + (void)consumeAttackEdge(); + input_vx_ = 0.0F; + input_vy_ = 0.0F; + intended_vx_ = 0.0F; + intended_vy_ = 0.0F; + motion_time_ms_ = 0.0F; + setVelX(0.0F); + setVelY(0.0F); +} - // Decrement del timer d'immunitat. Independent de l'estat: si el - // jugador mor durant la immunitat (només via forat, que no es gateja), - // el `respawn` rearma una nova finestra. Si la finestra acaba mentre - // és AIRBORNE/HURT, no passa res — el gate dels hazards passa a - // quedar obert al primer frame post-zero. - if (immunity_remaining_ms_ > 0.0F) { - immunity_remaining_ms_ -= delta_time * 1000.0F; - immunity_remaining_ms_ = std::max(immunity_remaining_ms_, 0.0F); +// En LANDING_STUN, descomptem el timer i ignorem qualsevol input +// (moviment i salt). Quan s'esgota, tornem a GROUNDED al pròxim +// frame. La velocitat queda a zero — després del stun hi tornarà +// a haver intenció si el jugador prem alguna tecla. +void Player::tickLandingStun(float delta_time) { + landing_stun_remaining_ms_ -= delta_time * 1000.0F; + if (landing_stun_remaining_ms_ <= 0.0F) { + landing_stun_remaining_ms_ = 0.0F; + state_ = State::GROUNDED; } + input_vx_ = 0.0F; + input_vy_ = 0.0F; + intended_vx_ = 0.0F; + intended_vy_ = 0.0F; + motion_time_ms_ = 0.0F; + setVelX(0.0F); + setVelY(0.0F); + // Mantenim els flancs del botó de salt actualitzats fins i tot + // durant el stun: així si l'usuari l'ha estat prement abans, no + // se'l consumeix com a salt nou en sortir del stun. + (void)consumeJumpEdge(); + (void)consumeAttackEdge(); +} - // DYING: animació `death_spinning` + pausa de respawn. Ignorem tot - // l'input (ni moviment, ni salt, ni atac). El moviment queda - // congelat al lloc de la mort; `applyMovement` avança l'animació i - // detecta la fi per disparar el respawn. Mantenim els flancs de - // botó actualitzats perquè una tecla sostinguda no es comporte com - // a flanc nou en sortir del cicle. - if (state_ == State::DYING) { - (void)consumeJumpEdge(); - (void)consumeAttackEdge(); - input_vx_ = 0.0F; - input_vy_ = 0.0F; - intended_vx_ = 0.0F; - intended_vy_ = 0.0F; - motion_time_ms_ = 0.0F; - setVelX(0.0F); - setVelY(0.0F); - return; +// En AIRBORNE el jugador no llig input direccional: la velocitat +// horitzontal queda fixada al moment del despegue. Tampoc canvia +// facing. Sí actualitzem els flancs del botó perquè la propera +// pulsació quan torne a terra es detecte com a flanc nou. +void Player::tickAirborne() { + (void)consumeJumpEdge(); + (void)consumeAttackEdge(); + setVelX(intended_vx_); + setVelY(intended_vy_); +} + +// En ATTACKING el jugador queda bloquejat tot el `swing_ms + +// cooldown_ms`: ni input direccional, ni salt, ni atac, ni canvi +// de facing. El timer descompta amb `delta_time * 1000` per +// mantenir la convenció de delta_ms de la resta d'estats temporals. +// Mantenim els flancs dels botons actualitzats perquè una +// tecla mantinguda no es comporte com a flanc nou en sortir. +void Player::tickAttacking(float delta_time) { + (void)consumeJumpEdge(); + (void)consumeAttackEdge(); + attacking_remaining_ms_ -= delta_time * 1000.0F; + if (attacking_remaining_ms_ <= 0.0F) { + attacking_remaining_ms_ = 0.0F; + state_ = State::GROUNDED; } + input_vx_ = 0.0F; + input_vy_ = 0.0F; + intended_vx_ = 0.0F; + intended_vy_ = 0.0F; + motion_time_ms_ = 0.0F; + setVelX(0.0F); + setVelY(0.0F); +} - // Reset per frame: el resolver del GameScene el tornarà a posar a - // true si aquest frame hi ha push actiu. Així quan el push acaba - // (un deixa d'empentar, l'altre comença a caminar), l'animació torna - // a governar-se per la velocitat real sense estat romanent. - being_pushed_ = false; - - // En LANDING_STUN, descomptem el timer i ignorem qualsevol input - // (moviment i salt). Quan s'esgota, tornem a GROUNDED al pròxim - // frame. La velocitat queda a zero — després del stun hi tornarà - // a haver intenció si el jugador prem alguna tecla. - if (state_ == State::LANDING_STUN) { - landing_stun_remaining_ms_ -= delta_time * 1000.0F; - if (landing_stun_remaining_ms_ <= 0.0F) { - landing_stun_remaining_ms_ = 0.0F; - state_ = State::GROUNDED; +// En HURT el jugador està projectat sense control: descomptem +// velocitat per inèrcia (decaiment lineal fins a 0). Quan arriba a +// zero, tornem a GROUNDED i el frame següent ja torna a llegir input. +// El moviment real l'aplica `applyMovement` (igual que els altres +// estats), que també gestiona el xoc contra murs. +void Player::tickHurt(float delta_time) { + (void)consumeJumpEdge(); + (void)consumeAttackEdge(); + const float DECEL = (cfg_.hurt.brake_time_ms > 0) + ? cfg_.hurt.initial_speed_px_per_s / (static_cast(cfg_.hurt.brake_time_ms) / 1000.0F) + : 0.0F; + const float STEP = DECEL * delta_time; + auto decay = [STEP](float& v) { + if (std::fabs(v) <= STEP) { + v = 0.0F; + } else { + v -= std::copysign(STEP, v); } - input_vx_ = 0.0F; - input_vy_ = 0.0F; - intended_vx_ = 0.0F; - intended_vy_ = 0.0F; - motion_time_ms_ = 0.0F; - setVelX(0.0F); - setVelY(0.0F); - // Mantenim els flancs del botó de salt actualitzats fins i tot - // durant el stun: així si l'usuari l'ha estat prement abans, no - // se'l consumeix com a salt nou en sortir del stun. - (void)consumeJumpEdge(); - (void)consumeAttackEdge(); - return; - } - - // En AIRBORNE el jugador no llig input direccional: la velocitat - // horitzontal queda fixada al moment del despegue. Tampoc canvia - // facing. Sí actualitzem els flancs del botó perquè la propera - // pulsació quan torne a terra es detecte com a flanc nou. - if (state_ == State::AIRBORNE) { - (void)consumeJumpEdge(); - (void)consumeAttackEdge(); - setVelX(intended_vx_); - setVelY(intended_vy_); - return; - } - - // En ATTACKING el jugador queda bloquejat tot el `swing_ms + - // cooldown_ms`: ni input direccional, ni salt, ni atac, ni canvi - // de facing. El timer descompta amb `delta_time * 1000` per - // mantenir la convenció de delta_ms de la resta d'estats temporals. - // Mantenim els flancs dels botons actualitzats perquè una - // tecla mantinguda no es comporte com a flanc nou en sortir. - if (state_ == State::ATTACKING) { - (void)consumeJumpEdge(); - (void)consumeAttackEdge(); - attacking_remaining_ms_ -= delta_time * 1000.0F; - if (attacking_remaining_ms_ <= 0.0F) { - attacking_remaining_ms_ = 0.0F; - state_ = State::GROUNDED; - } - input_vx_ = 0.0F; - input_vy_ = 0.0F; - intended_vx_ = 0.0F; - intended_vy_ = 0.0F; - motion_time_ms_ = 0.0F; - setVelX(0.0F); - setVelY(0.0F); - return; - } - - // En HURT el jugador està projectat sense control: descomptem - // velocitat per inèrcia (decaiment lineal fins a 0). Quan arriba a - // zero, tornem a GROUNDED i el frame següent ja torna a llegir input. - // El moviment real l'aplica `applyMovement` (igual que els altres - // estats), que també gestiona el xoc contra murs. - if (state_ == State::HURT) { - (void)consumeJumpEdge(); - (void)consumeAttackEdge(); - const float DECEL = (cfg_.hurt.brake_time_ms > 0) - ? cfg_.hurt.initial_speed_px_per_s / (static_cast(cfg_.hurt.brake_time_ms) / 1000.0F) - : 0.0F; - const float STEP = DECEL * delta_time; - auto decay = [STEP](float& v) { - if (std::fabs(v) <= STEP) { - v = 0.0F; - } else { - v -= std::copysign(STEP, v); - } - }; - decay(intended_vx_); - decay(intended_vy_); - if (intended_vx_ == 0.0F && intended_vy_ == 0.0F) { - state_ = State::GROUNDED; - } - input_vx_ = 0.0F; - input_vy_ = 0.0F; - setVelX(intended_vx_); - setVelY(intended_vy_); - return; + }; + decay(intended_vx_); + decay(intended_vy_); + if (intended_vx_ == 0.0F && intended_vy_ == 0.0F) { + state_ = State::GROUNDED; } + input_vx_ = 0.0F; + input_vy_ = 0.0F; + setVelX(intended_vx_); + setVelY(intended_vy_); +} +// Estat normal de joc (GROUNDED): llegeix input, integra física, actualitza +// facing i processa els flancs d'atac i salt. +void Player::tickGroundedInput(float delta_time) { // Refresca les vinculacions des d'Options en calent: així un remap fet // al menú de servei té efecte immediatament, sense esperar al pròxim // GameScene::onEnter. Replica el comportament d'AEE @@ -725,6 +697,55 @@ void Player::tick(float delta_time) { } } +// Dispatcher per estat. La complexitat real viu als handlers individuals. +void Player::tick(float delta_time) { + // EXITED/DISABLED: jugador fora del joc, no fa res. Cap input, cap + // física, cap bus event. Continua sent al vector de jugadors per + // simplicitat (no s'esborra) però queda completament inert fins + // que GameScene el reactiva (respawn / spawnFromAir). + if (isOutOfPlay()) { return; } + + // Decrement del timer d'immunitat. Independent de l'estat: si el + // jugador mor durant la immunitat (només via forat, que no es gateja), + // el `respawn` rearma una nova finestra. Si la finestra acaba mentre + // és AIRBORNE/HURT, no passa res — el gate dels hazards passa a + // quedar obert al primer frame post-zero. + if (immunity_remaining_ms_ > 0.0F) { + immunity_remaining_ms_ -= delta_time * 1000.0F; + immunity_remaining_ms_ = std::max(immunity_remaining_ms_, 0.0F); + } + + if (state_ == State::DYING) { + tickDying(); + return; + } + + // Reset per frame: el resolver del GameScene el tornarà a posar a + // true si aquest frame hi ha push actiu. Així quan el push acaba + // (un deixa d'empentar, l'altre comença a caminar), l'animació torna + // a governar-se per la velocitat real sense estat romanent. + being_pushed_ = false; + + if (state_ == State::LANDING_STUN) { + tickLandingStun(delta_time); + return; + } + if (state_ == State::AIRBORNE) { + tickAirborne(); + return; + } + if (state_ == State::ATTACKING) { + tickAttacking(delta_time); + return; + } + if (state_ == State::HURT) { + tickHurt(delta_time); + return; + } + + tickGroundedInput(delta_time); +} + void Player::refreshControlsFromOptions() { if (id_ != 0 && id_ != 1) { return; } const auto& kb = (id_ == 0) ? Options::controls().keyboard_p1 diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index a037a7e..00e50fb 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -604,6 +604,15 @@ class Player : public AnimatedSprite { void renderShadow() const; private: + // Sub-handlers de tick(), un per estat amb sortida prematura. Mantenir-los + // a part redueix la complexitat cognitiva del dispatcher principal. + void tickDying(); + void tickLandingStun(float delta_time); + void tickAirborne(); + void tickAttacking(float delta_time); + void tickHurt(float delta_time); + void tickGroundedInput(float delta_time); + // Alias privat històric. Mantingut perquè viu dins de l'implementació // (snapToLane, clamp). Els consumidors externs usen `getBaseBox()`. [[nodiscard]] auto baseBox() const -> SDL_FRect { return getBaseBox(); }