fix(hazard): aterrar damunt d'un Hole salta el LANDING_STUN

This commit is contained in:
JailDesigner committed 2026-04-30 13:53:26 +02:00
1 parent 4da50571eb
commit 3e5217066f
5 files changed
+80 -5

No files matched your search

+12
View File
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
#include <cstddef> #include <cstddef>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -74,6 +76,16 @@ class EntityManager {
// reflecteixi l'estat del frame actual. // reflecteixi l'estat del frame actual.
void checkPlayerCollisions(const std::vector<Player*>& players); void checkPlayerCollisions(const std::vector<Player*>& 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 // Fan-out del hook de pintat: per a cada sarcòfag, cridar
// notifyTilePainted. Les entitats no-sarcòfag ho ignoren per defecte // notifyTilePainted. Les entitats no-sarcòfag ho ignoren per defecte
// (veure Entity::notifyTilePainted). Invocat com a reacció a // (veure Entity::notifyTilePainted). Invocat com a reacció a
@@ -58,6 +58,23 @@ void EntityManager::renderAll() {
for (const auto& e : entities_) { renderEntity(*e); } 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<Player*>& players) { void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
if (players.empty()) { return; } if (players.empty()) { return; }
for (const auto& entity : entities_) { for (const auto& entity : entities_) {
+32 -5
View File
@@ -17,6 +17,8 @@
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.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/entities/event_bus.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
@@ -923,6 +925,28 @@ auto Player::integrateJump(float delta_time) -> bool {
return false; return false;
} }
if (jump_z_ <= 0.0F) { 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_z_ = 0.0F;
jump_vz_ = 0.0F; jump_vz_ = 0.0F;
state_ = State::LANDING_STUN; state_ = State::LANDING_STUN;
@@ -1455,11 +1479,14 @@ void Player::disable() {
} }
void Player::enterHoleFall(float center_x, float center_y, float floor_y) { void Player::enterHoleFall(float center_x, float center_y, float floor_y) {
// Defensa: només transicionem des de GROUNDED. Saltadors, AIRBORNE, // Cap guard de state ací: la responsabilitat la té el caller perquè
// HURT, DYING, etc. no han de caure pel forat (entre altres coses, // hi ha dos callers vàlids amb states distints:
// perquè `checkPlayerCollisions` ja filtra airborne via altura, però // - `Hole::onPlayerContact` (state GROUNDED, el jugador camina i
// afegim el gat ací per coherència amb la resta de transicions). // trepitja el tile del forat).
if (state_ != State::GROUNDED) { return; } // - `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. // 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: // Resolem `pos = center - (hit_offset + hit_size/2)` per a cada eix:
+15
View File
@@ -12,6 +12,7 @@
#include "game/map/map.hpp" #include "game/map/map.hpp"
class EventBus; class EventBus;
class EntityManager;
class Player : public AnimatedSprite { class Player : public AnimatedSprite {
public: public:
@@ -345,6 +346,14 @@ class Player : public AnimatedSprite {
// correspongui (sarcòfags). Si el bus no s'injecta, no emet res. // correspongui (sarcòfags). Si el bus no s'injecta, no emet res.
void setEventBus(EventBus* bus) { bus_ = bus; } 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 // Tile actual del jugador (centre de `baseBox()` convertit a
// coordenades de tile del tilemap, usant Map::tile_size i // coordenades de tile del tilemap, usant Map::tile_size i
// Map::tilemap_origin_*). Retorna (col, row). // Map::tilemap_origin_*). Retorna (col, row).
@@ -815,6 +824,12 @@ class Player : public AnimatedSprite {
// Player només en guarda un punter per publicar events. // Player només en guarda un punter per publicar events.
EventBus* bus_{nullptr}; 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è // Últim tile publicat. Inicialitzats a un sentinel fora de rang perquè
// el primer frame en què el bus estigui connectat ja emeti un event. // el primer frame en què el bus estigui connectat ja emeti un event.
int last_tile_col_{-9999}; int last_tile_col_{-9999};
+4
View File
@@ -407,6 +407,10 @@ GameScene::GameScene(Config config)
// el PaintLayer (subscrit ja) processa el pintat i el fan-out cap als // el PaintLayer (subscrit ja) processa el pintat i el fan-out cap als
// sarcòfags passa per `tile_painted`. // sarcòfags passa per `tile_painted`.
for (auto& p : players_) { p->setEventBus(&entity_manager_.getBus()); } 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 // 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 // com a propietat de la piràmide (0.30 per defecte). La vista plana