From 3e5217066fce6da5fe73ce32c632fcc60ee403e8 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 30 Apr 2026 13:53:26 +0200 Subject: [PATCH] fix(hazard): aterrar damunt d'un Hole salta el LANDING_STUN --- source/game/entities/entity_manager.hpp | 12 ++++++ .../entities/entity_manager_loadlevel.cpp | 17 +++++++++ source/game/entities/player.cpp | 37 ++++++++++++++++--- source/game/entities/player.hpp | 15 ++++++++ source/game/scenes/game_scene.cpp | 4 ++ 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/source/game/entities/entity_manager.hpp b/source/game/entities/entity_manager.hpp index 6893cb3..a2d2515 100644 --- a/source/game/entities/entity_manager.hpp +++ b/source/game/entities/entity_manager.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -74,6 +76,16 @@ class EntityManager { // reflecteixi l'estat del frame actual. void checkPlayerCollisions(const std::vector& players); + // Cerca la primera entitat que (a) sigui decal de sòl + // (`is_floor_decal_=true`), (b) admeti triggers d'àrea + // (`triggers_on_overlap_=true`), i (c) la seva caixa de col·lisió + // solape amb el rect donat. Pensat per a `Player::integrateJump`, + // que la consulta a l'aterratge per detectar caigudes sobre Hole + // (i en el futur pinchos plans, plaques de pressió, charcos...) i + // saltar-se el LANDING_STUN normal. Retorna `nullptr` si no hi ha + // cap match — el caller usa el camí d'aterratge habitual. + [[nodiscard]] auto findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity*; + // Fan-out del hook de pintat: per a cada sarcòfag, cridar // notifyTilePainted. Les entitats no-sarcòfag ho ignoren per defecte // (veure Entity::notifyTilePainted). Invocat com a reacció a diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index 7d25c40..9078964 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -58,6 +58,23 @@ void EntityManager::renderAll() { for (const auto& e : entities_) { renderEntity(*e); } } +auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* { + // Cerca lineal sobre `entities_`: cost menyspreable amb poques + // desenes d'entitats per sala i una sola crida per frame i per + // jugador (a l'aterratge). Si esdevé calent, es podria indexar + // per cel·la del tilemap. La doble condició filtra exclusivament + // decals de sòl que volen rebre triggers — avui Hole; en el + // futur pinchos plans, plaques de pressió, charcos. + for (const auto& entity : entities_) { + if (!entity->triggersOnOverlap()) { continue; } + if (!entity->isFloorDecal()) { continue; } + const auto BOX = entity->getCollisionRect(); + const bool OVERLAP = rect.x + rect.w > BOX.x && BOX.x + BOX.w > rect.x && rect.y + rect.h > BOX.y && BOX.y + BOX.h > rect.y; + if (OVERLAP) { return entity.get(); } + } + return nullptr; +} + void EntityManager::checkPlayerCollisions(const std::vector& players) { if (players.empty()) { return; } for (const auto& entity : entities_) { diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 2a673b5..4cc909f 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -17,6 +17,8 @@ #include "core/resources/resource_cache.hpp" #include "core/resources/resource_helper.hpp" #include "external/fkyaml_node.hpp" +#include "game/entities/entity.hpp" +#include "game/entities/entity_manager.hpp" #include "game/entities/event_bus.hpp" #include "game/options.hpp" #include "utils/defaults.hpp" @@ -923,6 +925,28 @@ auto Player::integrateJump(float delta_time) -> bool { return false; } if (jump_z_ <= 0.0F) { + // Aterratge sobre decal-trigger (Hole, futurs pinchos plans...): + // saltem el LANDING_STUN i transicionem directament a la caiguda + // pel forat. Sense aquesta detecció, l'aterratge entraria a + // LANDING_STUN (~200 ms flotant sobre el forat) i només quan + // tornés a GROUNDED `Hole::onPlayerContact` ho dispararia. La + // hit-box (8×8 centrada al sprite) és la mateixa que `Hole` + // usa al seu re-check; coherent. Si el manager no s'ha + // injectat (tests stand-alone), el camí d'aterratge segueix sent + // el habitual. + if (entity_manager_ != nullptr) { + const Entity* trigger = entity_manager_->findFloorTriggerAt(getHitBox()); + if (trigger != nullptr) { + const auto BOX = trigger->getCollisionRect(); + const float CENTER_X = BOX.x + (BOX.w * 0.5F); + const float CENTER_Y = BOX.y + (BOX.h * 0.5F); + const float FLOOR_Y = BOX.y + BOX.h; + enterHoleFall(CENTER_X, CENTER_Y, FLOOR_Y); + // No retornem `true`: no és un aterratge "normal" i + // `applyMovement` no ha de cridar `resolveLandingOverlap`. + return false; + } + } jump_z_ = 0.0F; jump_vz_ = 0.0F; state_ = State::LANDING_STUN; @@ -1455,11 +1479,14 @@ void Player::disable() { } void Player::enterHoleFall(float center_x, float center_y, float floor_y) { - // Defensa: només transicionem des de GROUNDED. Saltadors, AIRBORNE, - // HURT, DYING, etc. no han de caure pel forat (entre altres coses, - // perquè `checkPlayerCollisions` ja filtra airborne via altura, però - // afegim el gat ací per coherència amb la resta de transicions). - if (state_ != State::GROUNDED) { return; } + // Cap guard de state ací: la responsabilitat la té el caller perquè + // hi ha dos callers vàlids amb states distints: + // - `Hole::onPlayerContact` (state GROUNDED, el jugador camina i + // trepitja el tile del forat). + // - `Player::integrateJump` (state AIRBORNE, en aterrar damunt + // d'un decal-trigger en lloc d'entrar a LANDING_STUN). + // Posar `if (state_ != GROUNDED) return` ací bloquejaria el segon + // cas i obligaria a hacks tipus "forçar GROUNDED abans de cridar". // Snap del centre de la hit-box a la coordenada central del forat. // Resolem `pos = center - (hit_offset + hit_size/2)` per a cada eix: diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index b564463..e269240 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -12,6 +12,7 @@ #include "game/map/map.hpp" class EventBus; +class EntityManager; class Player : public AnimatedSprite { public: @@ -345,6 +346,14 @@ class Player : public AnimatedSprite { // correspongui (sarcòfags). Si el bus no s'injecta, no emet res. void setEventBus(EventBus* bus) { bus_ = bus; } + // Injecta el manager d'entitats del nivell. L'usa `integrateJump` per + // detectar aterratges sobre decals-trigger (forats, plaques de + // pressió...) i disparar la transició corresponent en lloc del + // LANDING_STUN normal. Propietat externa (mateixa convenció que + // `setEventBus`); si no s'injecta, l'aterratge segueix el camí + // normal (els tests stand-alone del Player no necessiten injectar-lo). + void setEntityManager(EntityManager* manager) { entity_manager_ = manager; } + // Tile actual del jugador (centre de `baseBox()` convertit a // coordenades de tile del tilemap, usant Map::tile_size i // Map::tilemap_origin_*). Retorna (col, row). @@ -815,6 +824,12 @@ class Player : public AnimatedSprite { // Player només en guarda un punter per publicar events. EventBus* bus_{nullptr}; + // Manager d'entitats del nivell (opcional). Propietat externa + // (GameScene); l'usa `integrateJump` per consultar + // `findFloorTriggerAt` a l'aterratge i decidir si el frame del LAND + // ha de disparar `enterHoleFall` en lloc d'un LANDING_STUN normal. + EntityManager* entity_manager_{nullptr}; + // Últim tile publicat. Inicialitzats a un sentinel fora de rang perquè // el primer frame en què el bus estigui connectat ja emeti un event. int last_tile_col_{-9999}; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 7df6a0a..9fac4dc 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -407,6 +407,10 @@ GameScene::GameScene(Config config) // el PaintLayer (subscrit ja) processa el pintat i el fan-out cap als // sarcòfags passa per `tile_painted`. for (auto& p : players_) { p->setEventBus(&entity_manager_.getBus()); } + // També injectem el manager perquè `integrateJump` pugui consultar + // `findFloorTriggerAt` a l'aterratge (caure dins d'un Hole en lloc + // del LANDING_STUN normal). + for (auto& p : players_) { p->setEntityManager(&entity_manager_); } // Física del contacte entre jugadors. El factor viu al YAML del mapa // com a propietat de la piràmide (0.30 per defecte). La vista plana