From cd50626b88d7fc01dd358cfef250c790fe967f95 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 25 Apr 2026 11:49:48 +0200 Subject: [PATCH] =?UTF-8?q?feat(collisions):=20filtre=20per=20altura=20?= =?UTF-8?q?=E2=80=94=20saltar=20esquiva=20momies,=20roques=20i=20altres=20?= =?UTF-8?q?jugadors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/entities/door/door.yaml | 4 ++++ data/entities/floor_trap/floor_trap.yaml | 5 +++++ data/entities/mummy/mummy.yaml | 5 +++++ data/entities/rolling_rock/rolling_rock.yaml | 5 +++++ data/entities/sarcophagus/sarcophagus.yaml | 7 +++++++ data/entities/torch/torch.yaml | 4 ++++ data/entities/wall_trap/wall_trap.yaml | 5 +++++ source/game/entities/entity.hpp | 12 +++++++++++ .../entities/entity_manager_loadlevel.cpp | 20 +++++++++++++++++++ source/game/entities/player.cpp | 11 +++++++++- source/game/scenes/game_scene.cpp | 11 ++++++++++ 11 files changed, 88 insertions(+), 1 deletion(-) diff --git a/data/entities/door/door.yaml b/data/entities/door/door.yaml index bf86231..fabd3d8 100644 --- a/data/entities/door/door.yaml +++ b/data/entities/door/door.yaml @@ -4,6 +4,10 @@ sprite: path: door.gif size: [48, 32] tile_size: [3, 2] +collision: + # Porta = mur infranquejable. Igual que el sarc, alçada efectiva + # infinita per impedir que un jugador la salte per damunt. + height: 999 flags: collides: true interactable: true diff --git a/data/entities/floor_trap/floor_trap.yaml b/data/entities/floor_trap/floor_trap.yaml index e7b2aa1..d99c006 100644 --- a/data/entities/floor_trap/floor_trap.yaml +++ b/data/entities/floor_trap/floor_trap.yaml @@ -4,6 +4,11 @@ sprite: path: floor_trap.gif size: [16, 16] tile_size: [1, 1] +collision: + # Trampa al sòl: alçada 0 → qualsevol salt l'esquiva. (Avui amb + # `collides: false` la trampa no entra al filtre de contacte, però + # declarem el camp per quan es cablege la detecció via àrea.) + height: 0 flags: collides: false interactable: false diff --git a/data/entities/mummy/mummy.yaml b/data/entities/mummy/mummy.yaml index f271a60..41f6bf3 100644 --- a/data/entities/mummy/mummy.yaml +++ b/data/entities/mummy/mummy.yaml @@ -14,6 +14,11 @@ anchor: feet collision: size: [16, 16] base_size: [16, 16] + # Alçada del cos en píxels per al filtre de col·lisió per salt: un + # jugador amb jump_z > height no col·lisiona amb la momia (la salta + # per damunt). Amb impulse=240 + gravity=600 el pic ronda els 48 px, + # així que 24 deixa marge per esquivar amb un salt no perfecte. + height: 24 flags: collides: true interactable: false diff --git a/data/entities/rolling_rock/rolling_rock.yaml b/data/entities/rolling_rock/rolling_rock.yaml index 326f5b5..95f3745 100644 --- a/data/entities/rolling_rock/rolling_rock.yaml +++ b/data/entities/rolling_rock/rolling_rock.yaml @@ -4,6 +4,11 @@ sprite: path: rolling_rock.gif size: [16, 16] tile_size: [1, 1] +collision: + # `size`/`base_size` no calen per a la roca (tot el sprite és cos), + # però sí declarem `height` per al filtre de salt: 16 px → fàcil + # d'esquivar amb un salt curt. + height: 16 flags: collides: true interactable: false diff --git a/data/entities/sarcophagus/sarcophagus.yaml b/data/entities/sarcophagus/sarcophagus.yaml index 7193f62..ab2e2ff 100644 --- a/data/entities/sarcophagus/sarcophagus.yaml +++ b/data/entities/sarcophagus/sarcophagus.yaml @@ -7,6 +7,13 @@ sprite: # són "alçada" en falsa perspectiva, sense col·lisió. Perímetre ortogonal # de 8 tiles (3 N + 3 S + 1 O + 1 E). tile_size: [3, 1] +collision: + # Sarcòfag = obstacle massís: alçada efectiva infinita (999) per al + # filtre del salt. Un jugador no el pot saltar per damunt encara que + # el salt arribe al punt àlgid. La caixa de col·lisió real (logical + # 48×16) la calcula `placeSarcophagus` a partir de la geometria del + # mapa, no des d'aquest YAML. + height: 999 flags: collides: true interactable: true diff --git a/data/entities/torch/torch.yaml b/data/entities/torch/torch.yaml index 6b00340..b659ead 100644 --- a/data/entities/torch/torch.yaml +++ b/data/entities/torch/torch.yaml @@ -4,6 +4,10 @@ sprite: path: torch.gif size: [16, 16] tile_size: [1, 1] +collision: + # Decoratiu, no col·lisiona. Camp `height` declarat per coherència + # amb la resta d'entitats; no té efecte mentre `collides: false`. + height: 0 flags: collides: false interactable: false diff --git a/data/entities/wall_trap/wall_trap.yaml b/data/entities/wall_trap/wall_trap.yaml index 2ef051b..39461c1 100644 --- a/data/entities/wall_trap/wall_trap.yaml +++ b/data/entities/wall_trap/wall_trap.yaml @@ -4,6 +4,11 @@ sprite: path: wall_trap.gif size: [16, 16] tile_size: [1, 1] +collision: + # Trampa de paret: ix d'una vora alta i no es pot saltar per damunt. + # 999 = infranquejable. Igual que floor_trap, el filtre només té efecte + # quan la detecció de contacte hi entre. + height: 999 flags: collides: false interactable: false diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index 66ebf8f..ca7a1c5 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -113,6 +113,17 @@ class Entity { // Entitats sense animació retornen `nullopt` → frame 0 per defecte. [[nodiscard]] auto getAnimation() const -> const std::optional& { return anim_; } + // Alçada del cos de l'entitat en píxels. La consulten els filtres + // de col·lisió per altura: si un jugador en l'aire passa per damunt + // d'aquest valor (jump_z_ > height), no col·lisiona amb l'entitat — + // és la mecànica del salt per esquivar momies, roques, etc. Valor + // 0 = sense altura (no es pot esquivar saltant; per a entitats amb + // collides=false el camp és irrellevant). Per a obstacles que + // representen "murs" (sarcs, portes), el YAML els pot donar un + // valor molt alt (p.ex. 999) per fer-los infranquejables. + [[nodiscard]] auto getHeight() const -> int { return height_; } + void setHeight(int h) { height_ = h; } + // Caixa de col·lisió: per defecte és el `pixel_rect_` sencer (entitats // AABB-aligned). Si s'ha configurat un `CollisionBox`, retorna el // sub-rect dins del sprite — vida útil: validar col·lisions amb el @@ -150,6 +161,7 @@ class Entity { std::shared_ptr surface_{nullptr}; bool collides_{false}; bool interactable_{false}; + int height_{0}; SDL_FlipMode flip_{SDL_FLIP_NONE}; EventBus* bus_{nullptr}; }; diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index 4d26778..c4b1cc7 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -63,8 +63,15 @@ void EntityManager::checkPlayerCollisions(const std::vector& players) { for (const auto& entity : entities_) { if (!entity->collides()) { continue; } const auto BOX = entity->getCollisionRect(); + const int E_HEIGHT = entity->getHeight(); for (auto* p : players) { if (p == nullptr) { continue; } + // Filtre per altura: si el jugador està en l'aire i està per + // damunt de l'alçada del cos de l'entitat, no hi ha contacte. + // És el mecanisme del salt per esquivar momies, roques, etc. + // Quan height és 0 (entitat sense altura declarada), el filtre + // no es dispara mai i la col·lisió funciona com abans. + if (p->isAirborne() && E_HEIGHT > 0 && p->getJumpZ() > static_cast(E_HEIGHT)) { continue; } // Contacte actor-actor amb hit-box petita: la caixa base // 16×16 servia per moure (xoc amb murs), però per a l'event // "toca una entitat" volem precisió — que la momia mate quan @@ -109,6 +116,18 @@ namespace { return info; } + // Llig `collision.height` (alçada del cos en píxels) de la plantilla. + // 0 si no està declarada — ho usen els filtres de col·lisió per altura + // del salt: un jugador per damunt d'aquest valor no col·lisiona amb + // l'entitat. Per a obstacles "tipus mur" (sarc, porta) el YAML els + // dóna un valor molt alt per fer-los infranquejables. + auto readHeight(const fkyaml::node& tmpl) -> int { + if (!tmpl.contains("collision")) { return 0; } + const auto& c = tmpl["collision"]; + if (!c.contains("height")) { return 0; } + return c["height"].get_value(); + } + // Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }` // de la plantilla d'entitat. // - Sense `base_size` ni `offset`: la caja queda alineada a la vora @@ -212,6 +231,7 @@ namespace { if (const auto BOX = readCollisionBox(tmpl, sprite_out.width, sprite_out.height); BOX.has_value()) { entity->setCollisionBox(*BOX); } + entity->setHeight(readHeight(tmpl)); anchor_out = readAnchor(tmpl); return entity; } diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 3f3d003..d59b377 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -300,9 +300,18 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool { // amb la hit-box actual de l'altre jugador. Així la caixa de moviment // (16×16) segueix topant amb murs/sarcs però els jugadors poden // acostar-se fins al contacte real. + // + // Filtre per altura: si aquest jugador (this) està en l'aire i passa + // per damunt del cos de l'altre, no hi ha col·lisió — saltar permet + // sobrevolar l'altre. `body_height` és estàtic (compartit entre + // jugadors), però per al filtre val la pena consultar-lo per actor + // perquè en el futur cada character podria tenir-ne un de propi. const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box); - return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT](const Player* p) { + const float Z = jump_z_; + const bool AIRBORNE = (state_ == State::AIRBORNE); + return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, AIRBORNE, Z](const Player* p) { if (p == nullptr || p == this) { return false; } + if (AIRBORNE && body_height > 0 && Z > static_cast(body_height)) { return false; } const auto OTHER = p->getHitBox(); return SDL_HasRectIntersectionFloat(&PROPOSED_HIT, &OTHER); }); diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 475facb..23fdbca 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -103,6 +103,17 @@ namespace { auto* b = ps[1]; if (a == nullptr || b == nullptr) { return; } + // Filtre per altura: si un dels dos està en l'aire per damunt + // del cos de l'altre, no hi ha contacte possible — el saltador + // sobrevola. Saltar fa de "moviment lliure" per al resolver. + // `body_height` és estàtic carregat des del YAML; cppcheck el llig + // com a sempre-zero perquè no veu la inicialització en runtime. + const auto BODY_H = static_cast(Player::body_height); + // cppcheck-suppress knownConditionTrueFalse + if (BODY_H > 0.0F && a->isAirborne() && a->getJumpZ() > BODY_H) { return; } + // cppcheck-suppress knownConditionTrueFalse + if (BODY_H > 0.0F && b->isAirborne() && b->getJumpZ() > BODY_H) { return; } + // Calculem l'overlap previst en l'espai de la hit-box petita: // els jugadors han de poder acostar-se molt (caixes de moviment // 16×16 quasi tocant) sense disparar el resolver; només quan el