feat(hazard): nou hazard Hole — caiguda per forat amb clip rect
This commit is contained in:
1 parent
d4593d0686
commit
4da50571eb
17 files changed
+376
-15
No files matched your search
@@ -50,6 +50,7 @@ set(ENTITY_SOURCES_BASE
|
|||||||
source/game/entities/concrete/rolling_rock.cpp
|
source/game/entities/concrete/rolling_rock.cpp
|
||||||
source/game/entities/concrete/wall_trap.cpp
|
source/game/entities/concrete/wall_trap.cpp
|
||||||
source/game/entities/concrete/floor_trap.cpp
|
source/game/entities/concrete/floor_trap.cpp
|
||||||
|
source/game/entities/concrete/hole.cpp
|
||||||
source/game/entities/concrete/dust.cpp
|
source/game/entities/concrete/dust.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -349,6 +350,7 @@ option(BUILD_TESTS "Build smoke tests" ON)
|
|||||||
if(BUILD_TESTS)
|
if(BUILD_TESTS)
|
||||||
add_executable(test_entities_smoke
|
add_executable(test_entities_smoke
|
||||||
tests/test_entities_smoke.cpp
|
tests/test_entities_smoke.cpp
|
||||||
|
tests/hole_player_stubs.cpp
|
||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
@@ -372,6 +374,7 @@ if(BUILD_TESTS)
|
|||||||
add_executable(test_whip_attack
|
add_executable(test_whip_attack
|
||||||
tests/test_whip_attack.cpp
|
tests/test_whip_attack.cpp
|
||||||
tests/whip_attack_player_stubs.cpp
|
tests/whip_attack_player_stubs.cpp
|
||||||
|
tests/hole_player_stubs.cpp
|
||||||
source/core/resources/resource_helper.cpp
|
source/core/resources/resource_helper.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
source/game/map/map.cpp
|
source/game/map/map.cpp
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 182 B |
@@ -0,0 +1,26 @@
|
|||||||
|
name: hole
|
||||||
|
type: static_hazard
|
||||||
|
sprite:
|
||||||
|
path: hole.gif
|
||||||
|
size: [16, 16]
|
||||||
|
tile_size: [1, 1]
|
||||||
|
collision:
|
||||||
|
# Forat: ocupa el tile sencer (16×16). `height: 0` deixa que un salt
|
||||||
|
# AIRBORNE el creui sense disparar el contacte (el filtre d'altura de
|
||||||
|
# `checkPlayerCollisions` el descarta quan `jump_z_ > height`). El
|
||||||
|
# bucle de contacte admet l'entitat per `flags.triggers_on_overlap`,
|
||||||
|
# encara que `flags.collides` sigui false.
|
||||||
|
size: [16, 16]
|
||||||
|
height: 0
|
||||||
|
flags:
|
||||||
|
collides: false
|
||||||
|
interactable: false
|
||||||
|
# Avui el loader no llig el bloc `flags:` (és declaratiu); el flag
|
||||||
|
# real es fixa al constructor C++ de `Hole`. Mantenim el camp per
|
||||||
|
# coherència amb la resta de plantilles i per quan es cablege la
|
||||||
|
# lectura genèrica de flags.
|
||||||
|
triggers_on_overlap: true
|
||||||
|
animations:
|
||||||
|
idle: { frames: [0], fps: 1, loop: true }
|
||||||
|
stats:
|
||||||
|
damage: 0
|
||||||
@@ -138,3 +138,9 @@ entities:
|
|||||||
position: [10, 7]
|
position: [10, 7]
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [7, 9]
|
position: [7, 9]
|
||||||
|
# Forat de prova: tile de sòl pla a la galeria superior, 2 tiles a
|
||||||
|
# l'est del punt d'aterratge del jugador (door_col=10). Lluny dels
|
||||||
|
# sarcs i de les momies; accessible caminant cap a l'est tot just
|
||||||
|
# després del spawn.
|
||||||
|
- type: hole
|
||||||
|
position: [12, 3]
|
||||||
@@ -73,6 +73,16 @@ animations:
|
|||||||
frame_ms: [60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280]
|
frame_ms: [60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280]
|
||||||
loop: false
|
loop: false
|
||||||
|
|
||||||
|
# Mort per caiguda a un agujero (DeathCause::HOLE). Frame i durada
|
||||||
|
# **placeholders** — pendents de Sergio: idealment l'índex de la
|
||||||
|
# casella buida del spritesheet i una durada curta. `loop: false`
|
||||||
|
# com `death_spinning` perquè `animationIsCompleted()` dispari el
|
||||||
|
# respawn timer determinísticament.
|
||||||
|
death_hole:
|
||||||
|
frames: [15]
|
||||||
|
frame_ms: 60
|
||||||
|
loop: false
|
||||||
|
|
||||||
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
|
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
|
||||||
# tocs curts (passets d'un tile sense rellisquera).
|
# tocs curts (passets d'un tile sense rellisquera).
|
||||||
inertia:
|
inertia:
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ animations:
|
|||||||
frame_ms: [60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280]
|
frame_ms: [60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280]
|
||||||
loop: false
|
loop: false
|
||||||
|
|
||||||
|
# Mort per caiguda a un agujero (DeathCause::HOLE). Frame i durada
|
||||||
|
# **placeholders** — pendents de Sergio: idealment l'índex de la
|
||||||
|
# casella buida del spritesheet i una durada curta. `loop: false`
|
||||||
|
# com `death_spinning` perquè `animationIsCompleted()` dispari el
|
||||||
|
# respawn timer determinísticament.
|
||||||
|
death_hole:
|
||||||
|
frames: [15]
|
||||||
|
frame_ms: 60
|
||||||
|
loop: false
|
||||||
|
|
||||||
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
|
# Inèrcia de frenada en soltar la direcció. `threshold_ms` filtra
|
||||||
# tocs curts (passets d'un tile sense rellisquera).
|
# tocs curts (passets d'un tile sense rellisquera).
|
||||||
inertia:
|
inertia:
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ FloorTrap::FloorTrap(const fkyaml::node& yaml) {
|
|||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("floor_trap");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("floor_trap");
|
||||||
collides_ = false;
|
collides_ = false;
|
||||||
interactable_ = false;
|
interactable_ = false;
|
||||||
|
// Trigger d'àrea: la trampa no bloqueja el pas però vol rebre
|
||||||
|
// `onPlayerContact` quan el jugador la solapa. Tanca el gap latent
|
||||||
|
// que la deixava muda (`checkPlayerCollisions` filtrava pel `collides`).
|
||||||
|
triggers_on_overlap_ = true;
|
||||||
|
|
||||||
if (yaml.contains("cycle")) {
|
if (yaml.contains("cycle")) {
|
||||||
const auto& c = yaml["cycle"];
|
const auto& c = yaml["cycle"];
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#include "game/entities/concrete/hole.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "game/entities/entity_factory.hpp"
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
|
|
||||||
|
Hole::Hole(const fkyaml::node& yaml) {
|
||||||
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("hole");
|
||||||
|
// Sense xoc físic ni interacció — només trigger d'àrea. El bucle de
|
||||||
|
// contacte ens admet via `triggers_on_overlap_=true`. Damage queda a
|
||||||
|
// 0 (no publiquem `player_hit`); la mort la dispara `integrateJump`
|
||||||
|
// del jugador quan supera `HOLE_FALL_DEPTH`.
|
||||||
|
collides_ = false;
|
||||||
|
interactable_ = false;
|
||||||
|
triggers_on_overlap_ = true;
|
||||||
|
// Decal de sòl: `Entity::getFeetY()` torna sentinel mínim si aquest
|
||||||
|
// flag està actiu — així el forat es pinta abans que qualsevol actor.
|
||||||
|
is_floor_decal_ = true;
|
||||||
|
setDamage(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Hole::onPlayerContact(Player* player) {
|
||||||
|
if (player == nullptr) { return; }
|
||||||
|
// Només mossega quan el jugador trepitja el forat de peu (GROUNDED).
|
||||||
|
// En l'aire, el filtre d'altura del bucle ja l'ha rebutjat (height_=0
|
||||||
|
// i jump_z_>0 prou per superar-ho); però mantenim el gat per defensa
|
||||||
|
// davant de futurs canvis al filtre.
|
||||||
|
if (player->getState() != Player::State::GROUNDED) { return; }
|
||||||
|
|
||||||
|
// Re-verificació estricta amb la hit-box interna del jugador (8×8):
|
||||||
|
// així el contacte només es dispara quan el centre del jugador ha
|
||||||
|
// entrat al tile del forat, no quan només el frega per la vora.
|
||||||
|
const SDL_FRect HIT = player->getHitBox();
|
||||||
|
const SDL_FRect BOX = getCollisionRect();
|
||||||
|
const bool OVERLAP = HIT.x + HIT.w > BOX.x && BOX.x + BOX.w > HIT.x && HIT.y + HIT.h > BOX.y && BOX.y + BOX.h > HIT.y;
|
||||||
|
if (!OVERLAP) { return; }
|
||||||
|
|
||||||
|
// Snap al centre XY del forat + Y de la base del tile (per al clip
|
||||||
|
// rect del sprite mentre cau). `enterHoleFall` rep coordenades
|
||||||
|
// absolutes en el canvas — el player s'encarrega de convertir a la
|
||||||
|
// seua convenció (cantó superior-esquerra del sprite).
|
||||||
|
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;
|
||||||
|
player->enterHoleFall(CENTER_X, CENTER_Y, FLOOR_Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
REGISTER_ENTITY("hole", Hole)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/hazard.hpp"
|
||||||
|
|
||||||
|
// Forat al sòl: hazard estàtic de 16×16 que NO bloqueja el pas però
|
||||||
|
// detecta quan el jugador hi cau a sobre. Característiques:
|
||||||
|
// - `collides_=false` + `triggers_on_overlap_=true`: rep `onPlayerContact`
|
||||||
|
// pel bucle de `EntityManager::checkPlayerCollisions` sense bloquejar
|
||||||
|
// el moviment del jugador.
|
||||||
|
// - `height_=0`: el filtre de col·lisió per altura existent deixa que
|
||||||
|
// un jugador AIRBORNE amb `jump_z_>0` el creui sense disparar res
|
||||||
|
// (saltar per damunt d'un forat).
|
||||||
|
// - Damage 0: el contacte no publica `player_hit`. En lloc d'això,
|
||||||
|
// `onPlayerContact` invoca `Player::enterHoleFall`, que transiciona
|
||||||
|
// el jugador a AIRBORNE amb subestat `falling_through_hole_`.
|
||||||
|
// - Verificació amb hit-box interna: `onPlayerContact` re-verifica
|
||||||
|
// l'overlap amb `Player::getHitBox()` (8×8 centrada al sprite),
|
||||||
|
// així només es dispara quan el centre del jugador entra al tile.
|
||||||
|
// - Z-order: `is_floor_decal_=true` (heretat) — `Entity::getFeetY()`
|
||||||
|
// retorna sentinel mínim, així el forat es pinta abans que qualsevol
|
||||||
|
// actor i les momies passen per davant. L'efecte "el sòl es traga el
|
||||||
|
// jugador" no es fa amb Y-sort: el `Player::render` aplica un clip
|
||||||
|
// rect mentre `falling_through_hole_=true` que retalla el sprite per
|
||||||
|
// sota de la base del tile del forat.
|
||||||
|
class Hole : public StaticHazard {
|
||||||
|
public:
|
||||||
|
explicit Hole(const fkyaml::node& yaml);
|
||||||
|
|
||||||
|
// No publica `player_hit`. En el seu lloc, dispara
|
||||||
|
// `Player::enterHoleFall(center_x, center_y, floor_y)` perquè el
|
||||||
|
// jugador armi el clip rect a la Y de la base del forat.
|
||||||
|
void onPlayerContact(Player* player) override;
|
||||||
|
};
|
||||||
@@ -10,6 +10,9 @@ WallTrap::WallTrap(const fkyaml::node& yaml) {
|
|||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("wall_trap");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("wall_trap");
|
||||||
collides_ = false;
|
collides_ = false;
|
||||||
interactable_ = false;
|
interactable_ = false;
|
||||||
|
// Trigger d'àrea: igual que FloorTrap. Tanca el gap latent del filtre
|
||||||
|
// de `checkPlayerCollisions` que ho deixava mut.
|
||||||
|
triggers_on_overlap_ = true;
|
||||||
|
|
||||||
if (yaml.contains("cycle")) {
|
if (yaml.contains("cycle")) {
|
||||||
const auto& c = yaml["cycle"];
|
const auto& c = yaml["cycle"];
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "game/entities/entity.hpp"
|
#include "game/entities/entity.hpp"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
Entity::Entity(std::string name, TilePos tile, SDL_FRect pixel_rect)
|
Entity::Entity(std::string name, TilePos tile, SDL_FRect pixel_rect)
|
||||||
@@ -38,6 +39,12 @@ auto Entity::getCollisionRect() const -> SDL_FRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto Entity::getFeetY() const -> float {
|
auto Entity::getFeetY() const -> float {
|
||||||
|
// Decal de sòl: sentinel mínim per quedar al principi del Y-sort
|
||||||
|
// ascendent de `GameScene` (stable_sort) i així pintar-se ABANS de
|
||||||
|
// qualsevol actor. Sense passades de render extra.
|
||||||
|
if (is_floor_decal_) {
|
||||||
|
return std::numeric_limits<float>::lowest();
|
||||||
|
}
|
||||||
const auto RECT = getCollisionRect();
|
const auto RECT = getCollisionRect();
|
||||||
return RECT.y + RECT.h;
|
return RECT.y + RECT.h;
|
||||||
}
|
}
|
||||||
@@ -88,6 +88,27 @@ class Entity {
|
|||||||
[[nodiscard]] auto collides() const -> bool { return collides_; }
|
[[nodiscard]] auto collides() const -> bool { return collides_; }
|
||||||
[[nodiscard]] auto interactable() const -> bool { return interactable_; }
|
[[nodiscard]] auto interactable() const -> bool { return interactable_; }
|
||||||
|
|
||||||
|
// Trigger d'àrea: l'entitat NO bloqueja el moviment (`collides_=false`)
|
||||||
|
// però sí vol rebre `onPlayerContact` quan el jugador la solapa. Pensat
|
||||||
|
// per a hazards "no físics" (forats al sòl, picos retràctils, plaques
|
||||||
|
// de pressió). El bucle de contacte (`checkPlayerCollisions`) admet
|
||||||
|
// l'entitat si compleix `collides_ || triggers_on_overlap_`. El filtre
|
||||||
|
// d'altura per salt (`height_`) i la caixa de col·lisió funcionen igual.
|
||||||
|
[[nodiscard]] auto triggersOnOverlap() const -> bool { return triggers_on_overlap_; }
|
||||||
|
void setTriggersOnOverlap(bool v) { triggers_on_overlap_ = v; }
|
||||||
|
|
||||||
|
// Decal de sòl: hazards estàtics sense presència vertical (forats,
|
||||||
|
// plaques de pressió, charcos). Es pinten ABANS de qualsevol actor
|
||||||
|
// dins del Y-sort de `GameScene` perquè conceptualment són "pintura
|
||||||
|
// sobre el tile". Implementació: `getFeetY()` retorna un sentinel
|
||||||
|
// mínim (`std::numeric_limits<float>::lowest()`) per quedar al
|
||||||
|
// principi del `stable_sort` ascendent — així no calen passades de
|
||||||
|
// render extra. Les subclasses (p.ex. `Hole`) poden sobreescriure
|
||||||
|
// `getFeetY` per saltar-se el sentinel en moments concrets (p.ex.
|
||||||
|
// un forat que està tragant un jugador i ha de pintar-se al damunt).
|
||||||
|
[[nodiscard]] auto isFloorDecal() const -> bool { return is_floor_decal_; }
|
||||||
|
void setFloorDecal(bool v) { is_floor_decal_ = v; }
|
||||||
|
|
||||||
// Marca per a entitats transitòries: efectes (polvet, espurnes,
|
// Marca per a entitats transitòries: efectes (polvet, espurnes,
|
||||||
// explosions...) que viuen una sola animació i s'autoeliminen quan
|
// explosions...) que viuen una sola animació i s'autoeliminen quan
|
||||||
// acaba. L'`EntityManager` purga totes les entitats amb `dead_=true`
|
// acaba. L'`EntityManager` purga totes les entitats amb `dead_=true`
|
||||||
@@ -172,6 +193,8 @@ class Entity {
|
|||||||
std::shared_ptr<Surface> surface_{nullptr};
|
std::shared_ptr<Surface> surface_{nullptr};
|
||||||
bool collides_{false};
|
bool collides_{false};
|
||||||
bool interactable_{false};
|
bool interactable_{false};
|
||||||
|
bool triggers_on_overlap_{false};
|
||||||
|
bool is_floor_decal_{false};
|
||||||
int height_{0};
|
int height_{0};
|
||||||
bool dead_{false};
|
bool dead_{false};
|
||||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||||
|
|||||||
@@ -61,7 +61,12 @@ void EntityManager::renderAll() {
|
|||||||
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_) {
|
||||||
if (!entity->collides()) { continue; }
|
// Acceptem entitats sòlides (xoc físic) i triggers d'àrea (no
|
||||||
|
// bloquegen però volen rebre `onPlayerContact`). Tanquem el gap
|
||||||
|
// latent de FloorTrap/WallTrap (que tenien `collides_=false` i
|
||||||
|
// mai no es disparaven) i obrim la porta a hazards no-físics
|
||||||
|
// com `Hole`.
|
||||||
|
if (!entity->collides() && !entity->triggersOnOverlap()) { continue; }
|
||||||
const auto BOX = entity->getCollisionRect();
|
const auto BOX = entity->getCollisionRect();
|
||||||
const int E_HEIGHT = entity->getHeight();
|
const int E_HEIGHT = entity->getHeight();
|
||||||
for (auto* p : players) {
|
for (auto* p : players) {
|
||||||
@@ -69,6 +74,10 @@ void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
|||||||
// contacte amb hazards (un segon toc de mòmia mentre la roda
|
// contacte amb hazards (un segon toc de mòmia mentre la roda
|
||||||
// ja és no-op; cap altra entitat l'ha de fer transicionar).
|
// ja és no-op; cap altra entitat l'ha de fer transicionar).
|
||||||
if (p == nullptr || p->isOutOfPlay() || p->isDying()) { continue; }
|
if (p == nullptr || p->isOutOfPlay() || p->isDying()) { continue; }
|
||||||
|
// Mentre el jugador cau per un forat, ignorem qualsevol altre
|
||||||
|
// hazard: la transició a `enterDying()` ja la dispara
|
||||||
|
// `integrateJump` quan supera `HOLE_FALL_DEPTH`.
|
||||||
|
if (p->isFallingThroughHole()) { continue; }
|
||||||
// Filtre per altura: si el jugador està en l'aire i està per
|
// 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.
|
// damunt de l'alçada del cos de l'entitat, no hi ha contacte.
|
||||||
// És el mecanisme del salt per esquivar momies, roques, etc.
|
// És el mecanisme del salt per esquivar momies, roques, etc.
|
||||||
|
|||||||
+117
-11
@@ -12,6 +12,7 @@
|
|||||||
#include "core/input/gamepad_bindings.hpp"
|
#include "core/input/gamepad_bindings.hpp"
|
||||||
#include "core/input/gamepad_manager.hpp"
|
#include "core/input/gamepad_manager.hpp"
|
||||||
#include "core/input/input_state.hpp"
|
#include "core/input/input_state.hpp"
|
||||||
|
#include "core/rendering/canvas.hpp"
|
||||||
#include "core/rendering/screen.hpp"
|
#include "core/rendering/screen.hpp"
|
||||||
#include "core/resources/resource_cache.hpp"
|
#include "core/resources/resource_cache.hpp"
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
@@ -910,6 +911,17 @@ auto Player::integrateJump(float delta_time) -> bool {
|
|||||||
if (state_ != State::AIRBORNE) { return false; }
|
if (state_ != State::AIRBORNE) { return false; }
|
||||||
jump_vz_ -= jump_config.gravity_px_per_s2 * delta_time;
|
jump_vz_ -= jump_config.gravity_px_per_s2 * delta_time;
|
||||||
jump_z_ += jump_vz_ * delta_time;
|
jump_z_ += jump_vz_ * delta_time;
|
||||||
|
// Caiguda per agujero: el sòl ja no és present sota el jugador, així
|
||||||
|
// que NO clampem `jump_z_` a 0. Deixem baixar a negatius (visualment
|
||||||
|
// el sprite "s'enfonsa" sota el sòl) i, en superar `HOLE_FALL_DEPTH`,
|
||||||
|
// disparem la mort. La cinemàtica de respawn (`enterDying →
|
||||||
|
// spawnFromAir`) reseteja el flag i la posició al spawn point.
|
||||||
|
if (falling_through_hole_) {
|
||||||
|
if (jump_z_ <= -HOLE_FALL_DEPTH) {
|
||||||
|
enterDying(DeathCause::HOLE);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (jump_z_ <= 0.0F) {
|
if (jump_z_ <= 0.0F) {
|
||||||
jump_z_ = 0.0F;
|
jump_z_ = 0.0F;
|
||||||
jump_vz_ = 0.0F;
|
jump_vz_ = 0.0F;
|
||||||
@@ -1075,7 +1087,7 @@ void Player::applyMovement(float delta_time) {
|
|||||||
if (state_ == State::GROUNDED) { publishTileEnteredIfChanged(); }
|
if (state_ == State::GROUNDED) { publishTileEnteredIfChanged(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::enterDying() {
|
void Player::enterDying(DeathCause cause) {
|
||||||
// Idempotent. Un segon contacte de momia mentre el jugador ja està
|
// Idempotent. Un segon contacte de momia mentre el jugador ja està
|
||||||
// en DYING no reinicia l'animació ni el comptador. Tampoc disparem
|
// en DYING no reinicia l'animació ni el comptador. Tampoc disparem
|
||||||
// el cicle si el jugador està fora de joc (EXITED al passar la
|
// el cicle si el jugador està fora de joc (EXITED al passar la
|
||||||
@@ -1091,6 +1103,8 @@ void Player::enterDying() {
|
|||||||
being_pushed_ = false;
|
being_pushed_ = false;
|
||||||
jump_z_ = 0.0F;
|
jump_z_ = 0.0F;
|
||||||
jump_vz_ = 0.0F;
|
jump_vz_ = 0.0F;
|
||||||
|
falling_through_hole_ = false;
|
||||||
|
hole_clip_bottom_y_ = 0.0F;
|
||||||
landing_stun_remaining_ms_ = 0.0F;
|
landing_stun_remaining_ms_ = 0.0F;
|
||||||
attacking_remaining_ms_ = 0.0F;
|
attacking_remaining_ms_ = 0.0F;
|
||||||
turning_remaining_ms_ = 0.0F;
|
turning_remaining_ms_ = 0.0F;
|
||||||
@@ -1102,7 +1116,18 @@ void Player::enterDying() {
|
|||||||
|
|
||||||
death_animation_complete_ = false;
|
death_animation_complete_ = false;
|
||||||
respawn_remaining_ms_ = 0.0F;
|
respawn_remaining_ms_ = 0.0F;
|
||||||
setCurrentAnimation("death_spinning");
|
// Presentació segons la causa. Cada `DeathCause` té la seua animació
|
||||||
|
// dedicada al YAML del player; `applyDyingMovement` arma el respawn
|
||||||
|
// timer quan `animationIsCompleted()` flanqueja, igual per a totes.
|
||||||
|
switch (cause) {
|
||||||
|
case DeathCause::HOLE:
|
||||||
|
setCurrentAnimation("death_hole");
|
||||||
|
break;
|
||||||
|
case DeathCause::GENERIC:
|
||||||
|
default:
|
||||||
|
setCurrentAnimation("death_spinning");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// So del cop letal: reaprofitem `hurt.wav` (ja precarregat per
|
// So del cop letal: reaprofitem `hurt.wav` (ja precarregat per
|
||||||
// `readHurtBlock`). El bloc `death:` no té camp de so propi —
|
// `readHurtBlock`). El bloc `death:` no té camp de so propi —
|
||||||
@@ -1204,19 +1229,46 @@ void Player::render() {
|
|||||||
if (state_ == State::DYING && death_animation_complete_) { return; }
|
if (state_ == State::DYING && death_animation_complete_) { return; }
|
||||||
|
|
||||||
// Mentre el jugador està enlaire, el sprite es desplaça visualment
|
// Mentre el jugador està enlaire, el sprite es desplaça visualment
|
||||||
// cap amunt `jump_z_` píxels. Modifiquem `pos_.y` (la posició entera
|
// `jump_z_` píxels: cap amunt si és positiu (salt), cap avall si és
|
||||||
|
// negatiu (caiguda per `Hole`, que deixa baixar `jump_z_` a negatius
|
||||||
|
// fins a `-HOLE_FALL_DEPTH`). Modifiquem `pos_.y` (la posició entera
|
||||||
// de render) abans de cridar el render base, i la restaurem després
|
// de render) abans de cridar el render base, i la restaurem després
|
||||||
// perquè l'estat seguent del frame ho recalcule des de `y_` (la font
|
// perquè l'estat seguent del frame ho recalcule des de `y_` (la font
|
||||||
// de veritat float). El sòl sota el jugador no es pinta des d'ací:
|
// de veritat float). El sòl sota el jugador no es pinta des d'ací:
|
||||||
// això ho fa el GameScene en una passada de sombres a banda.
|
// això ho fa el GameScene en una passada de sombres a banda. El cas
|
||||||
if (jump_z_ <= 0.0F) {
|
// `jump_z_ == 0` (a terra) surt per l'early-return per evitar tocar
|
||||||
AnimatedSprite::render();
|
// `pos_.y` innecessàriament.
|
||||||
return;
|
// Caiguda per agujero: el sprite es "menja" pel sòl. Fixem un clip
|
||||||
|
// global a la franja [0, hole_clip_bottom_y_] del canvas (el blitter
|
||||||
|
// de `Surface` respecta `Rendering::activeClip()`); a mesura que
|
||||||
|
// `pos_.y - jump_z_` baixa, els píxels del sprite que creuen la
|
||||||
|
// línia deixen de pintar-se. Fem el push/pop al voltant de tot el
|
||||||
|
// render del sprite per cobrir tant el cas `jump_z_==0` (caiguda
|
||||||
|
// just iniciada) com els frames posteriors. `pushClip` ja clamp
|
||||||
|
// a la mida del canvas, així que un `h` negatiu es traduiria en
|
||||||
|
// una franja buida i el sprite quedaria invisible — comportament
|
||||||
|
// correcte si el clip ja ha "tapat" tota la pantalla.
|
||||||
|
const bool CLIP_HOLE = falling_through_hole_;
|
||||||
|
if (CLIP_HOLE) {
|
||||||
|
const auto CANVAS = Rendering::activeCanvas();
|
||||||
|
Rendering::pushClip({
|
||||||
|
.x = 0,
|
||||||
|
.y = 0,
|
||||||
|
.w = CANVAS.width,
|
||||||
|
.h = static_cast<int>(hole_clip_bottom_y_),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const float SAVED_Y = pos_.y;
|
|
||||||
pos_.y = SAVED_Y - jump_z_;
|
if (jump_z_ == 0.0F) {
|
||||||
AnimatedSprite::render();
|
AnimatedSprite::render();
|
||||||
pos_.y = SAVED_Y;
|
} else {
|
||||||
|
const float SAVED_Y = pos_.y;
|
||||||
|
pos_.y = SAVED_Y - jump_z_;
|
||||||
|
AnimatedSprite::render();
|
||||||
|
pos_.y = SAVED_Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CLIP_HOLE) { Rendering::popClip(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::renderShadow() const {
|
void Player::renderShadow() const {
|
||||||
@@ -1224,6 +1276,10 @@ void Player::renderShadow() const {
|
|||||||
// que poden cridar `renderShadow` per a tots els jugadors sense
|
// que poden cridar `renderShadow` per a tots els jugadors sense
|
||||||
// discriminar.
|
// discriminar.
|
||||||
if (!isAirborne()) { return; }
|
if (!isAirborne()) { return; }
|
||||||
|
// Caient per un agujero: el cos s'enfonsa sota el sòl, dibuixar una
|
||||||
|
// sombra al ras del sòl trenca la il·lusió. Suprimim la sombra fins
|
||||||
|
// que `enterDying` netegi el flag.
|
||||||
|
if (falling_through_hole_) { return; }
|
||||||
|
|
||||||
// Sombra centrada al sprite, recolzada a la línia dels peus reals
|
// Sombra centrada al sprite, recolzada a la línia dels peus reals
|
||||||
// (ignorant `jump_z_`). L'amplada es redueix proporcional a l'altura
|
// (ignorant `jump_z_`). L'amplada es redueix proporcional a l'altura
|
||||||
@@ -1359,6 +1415,8 @@ void Player::respawn(float x, float y) {
|
|||||||
being_pushed_ = false;
|
being_pushed_ = false;
|
||||||
jump_z_ = 0.0F;
|
jump_z_ = 0.0F;
|
||||||
jump_vz_ = 0.0F;
|
jump_vz_ = 0.0F;
|
||||||
|
falling_through_hole_ = false;
|
||||||
|
hole_clip_bottom_y_ = 0.0F;
|
||||||
landing_stun_remaining_ms_ = 0.0F;
|
landing_stun_remaining_ms_ = 0.0F;
|
||||||
attacking_remaining_ms_ = 0.0F;
|
attacking_remaining_ms_ = 0.0F;
|
||||||
turning_remaining_ms_ = 0.0F;
|
turning_remaining_ms_ = 0.0F;
|
||||||
@@ -1396,6 +1454,54 @@ void Player::disable() {
|
|||||||
last_input_cardinal_ = Cardinal::NONE;
|
last_input_cardinal_ = Cardinal::NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
// així el centre exacte de la caja de contacte queda alineat amb
|
||||||
|
// (center_x, center_y), independentment de la geometria interna del
|
||||||
|
// sprite. `setPosX/Y` és l'API canònica per teleportar el jugador
|
||||||
|
// (la usen `respawn` i `spawnFromAir`).
|
||||||
|
const float HIT_HALF = static_cast<float>(hit_size) * 0.5F;
|
||||||
|
setPosX(center_x - static_cast<float>(hit_x_offset) - HIT_HALF);
|
||||||
|
setPosY(center_y - static_cast<float>(hit_y_offset) - HIT_HALF);
|
||||||
|
|
||||||
|
// Frenem qualsevol inèrcia prèvia: la caiguda ha de ser estàtica al
|
||||||
|
// centre del forat.
|
||||||
|
intended_vx_ = 0.0F;
|
||||||
|
intended_vy_ = 0.0F;
|
||||||
|
input_vx_ = 0.0F;
|
||||||
|
input_vy_ = 0.0F;
|
||||||
|
motion_time_ms_ = 0.0F;
|
||||||
|
being_pushed_ = false;
|
||||||
|
setVelX(0.0F);
|
||||||
|
setVelY(0.0F);
|
||||||
|
|
||||||
|
jump_z_ = 0.0F;
|
||||||
|
jump_vz_ = 0.0F;
|
||||||
|
state_ = State::AIRBORNE;
|
||||||
|
falling_through_hole_ = true;
|
||||||
|
// Línia visual on el clip rect ha de retallar el sprite. Restem
|
||||||
|
// `SPRITE_FLOOR_OFFSET` perquè la base del sprite té píxels
|
||||||
|
// transparents — el retall ha de coincidir amb la silueta visible,
|
||||||
|
// no amb la vora del rect del sprite.
|
||||||
|
hole_clip_bottom_y_ = floor_y - SPRITE_FLOOR_OFFSET;
|
||||||
|
|
||||||
|
// Animació de caiguda: respectem el `facing_` que ja portava el jugador
|
||||||
|
// (no té sentit reorientar-lo cap al sud només perquè caigui en un
|
||||||
|
// forat). Com `state_` ja és AIRBORNE, `animationName(false)` resol el
|
||||||
|
// prefix `jump_` i hi afegeix el sufix cardinal correcte —
|
||||||
|
// `jump_north`, `jump_south`, `jump_east` o `jump_west` segons el
|
||||||
|
// facing actual. Si demà cal una animació de "cara de pànic", només
|
||||||
|
// cal canviar aquí pel `setCurrentAnimation` corresponent.
|
||||||
|
setCurrentAnimation(animationName(false));
|
||||||
|
}
|
||||||
|
|
||||||
void Player::spawnFromAir(float x, float y) {
|
void Player::spawnFromAir(float x, float y) {
|
||||||
// Net total: equival a un `respawn` seguit d'una entrada en l'aire.
|
// Net total: equival a un `respawn` seguit d'una entrada en l'aire.
|
||||||
// Així el flag de gir, l'animació, els flancs del salt i les
|
// Així el flag de gir, l'animació, els flancs del salt i les
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ class Player : public AnimatedSprite {
|
|||||||
// que decau per inèrcia fins a parar (~2 tiles).
|
// que decau per inèrcia fins a parar (~2 tiles).
|
||||||
// No col·lisiona amb cap altre jugador. Quan
|
// No col·lisiona amb cap altre jugador. Quan
|
||||||
// s'atura, torna a GROUNDED.
|
// s'atura, torna a GROUNDED.
|
||||||
|
// Causa de la mort actual. La passa el caller a `enterDying(cause)` per
|
||||||
|
// triar la presentació visual (`death_<key>` al YAML del player). El
|
||||||
|
// default `GENERIC` cobreix tots els callsites antics (mòmies, etc.)
|
||||||
|
// sense haver de tocar-los; els nous (caiguda per `Hole`, en el futur
|
||||||
|
// pinchos / timeout / etc.) passen la seua causa explícita.
|
||||||
|
enum class DeathCause : std::uint8_t {
|
||||||
|
GENERIC, // mort estàndard — animació `death_spinning`
|
||||||
|
HOLE, // caiguda per agujero — animació `death_hole`
|
||||||
|
};
|
||||||
|
|
||||||
enum class State : std::uint8_t {
|
enum class State : std::uint8_t {
|
||||||
GROUNDED,
|
GROUNDED,
|
||||||
AIRBORNE,
|
AIRBORNE,
|
||||||
@@ -226,6 +236,21 @@ class Player : public AnimatedSprite {
|
|||||||
return state_ == State::GROUNDED || state_ == State::LANDING_STUN;
|
return state_ == State::GROUNDED || state_ == State::LANDING_STUN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// El jugador està caient per un agujero (`Hole`): subestat dins
|
||||||
|
// d'AIRBORNE amb gravetat aplicada SENSE clamp a 0 — `jump_z_` baixa
|
||||||
|
// a negatius fins a `HOLE_FALL_DEPTH`, llavors `enterDying()`. Mentre
|
||||||
|
// dura, `checkPlayerCollisions` salta aquest jugador (cap més hazard
|
||||||
|
// l'ha de tocar). Es neteja a `enterDying()` i a `respawn()`.
|
||||||
|
[[nodiscard]] auto isFallingThroughHole() const -> bool { return falling_through_hole_; }
|
||||||
|
|
||||||
|
// Activa la caída per agujero: snap del jugador al centre (cx, cy) en
|
||||||
|
// coordenades absolutes del canvas, transició a AIRBORNE amb
|
||||||
|
// `jump_z_=0` i `falling_through_hole_=true`, i arma el clip rect
|
||||||
|
// visual a partir de `floor_y` (Y de la base del tile del forat,
|
||||||
|
// canvas píxels). Desactiva input direccional reusant la branca
|
||||||
|
// AIRBORNE existent. No-op si el jugador no està a GROUNDED.
|
||||||
|
void enterHoleFall(float center_x, float center_y, float floor_y);
|
||||||
|
|
||||||
// Despawn lògic: el jugador acaba de creuar la porta de sortida.
|
// Despawn lògic: el jugador acaba de creuar la porta de sortida.
|
||||||
// Idempotent — només la primera crida fa la transició; les
|
// Idempotent — només la primera crida fa la transició; les
|
||||||
// posteriors són no-op. La publicació del `player_exited` al bus
|
// posteriors són no-op. La publicació del `player_exited` al bus
|
||||||
@@ -254,7 +279,7 @@ class Player : public AnimatedSprite {
|
|||||||
// momia mentre la roda no la reinicia). Quan l'animació acaba i
|
// momia mentre la roda no la reinicia). Quan l'animació acaba i
|
||||||
// `respawn_delay_ms` expira, `respawn(spawn_x_, spawn_y_)` torna a
|
// `respawn_delay_ms` expira, `respawn(spawn_x_, spawn_y_)` torna a
|
||||||
// GROUNDED i `lives_remaining_` decreix en 1 (pot anar a negatius).
|
// GROUNDED i `lives_remaining_` decreix en 1 (pot anar a negatius).
|
||||||
void enterDying();
|
void enterDying(DeathCause cause = DeathCause::GENERIC);
|
||||||
|
|
||||||
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
// Coordenades del spawn de respawn (cantó superior-esquerra del
|
||||||
// sprite, mateixa convenció que `respawn(x, y)`). El GameScene les
|
// sprite, mateixa convenció que `respawn(x, y)`). El GameScene les
|
||||||
@@ -655,6 +680,31 @@ class Player : public AnimatedSprite {
|
|||||||
float jump_z_{0.0F};
|
float jump_z_{0.0F};
|
||||||
float jump_vz_{0.0F};
|
float jump_vz_{0.0F};
|
||||||
|
|
||||||
|
// Subestat dins d'AIRBORNE: `enterHoleFall` el posa a true; mentre dura,
|
||||||
|
// `integrateJump` no clampa `jump_z_` a 0 — deixa baixar a negatius i
|
||||||
|
// dispara `enterDying()` quan creua `HOLE_FALL_DEPTH`. Es neteja a
|
||||||
|
// `enterDying()` i `respawn()`.
|
||||||
|
bool falling_through_hole_{false};
|
||||||
|
|
||||||
|
// Y absoluta (canvas píxels) per sota de la qual el `Player::render`
|
||||||
|
// retalla el sprite mentre `falling_through_hole_=true`. La fixa
|
||||||
|
// `enterHoleFall` a `floor_y - SPRITE_FLOOR_OFFSET` i la neteja
|
||||||
|
// `respawn`. Sense ús quan no estem caient per un forat.
|
||||||
|
float hole_clip_bottom_y_{0.0F};
|
||||||
|
|
||||||
|
// Profunditat (en píxels per sota del sòl) que ha de superar el jugador
|
||||||
|
// mentre cau per un agujero abans que `integrateJump` el mate. Constant
|
||||||
|
// per ara (ajustable a posteriori si el feel no és bo).
|
||||||
|
static constexpr float HOLE_FALL_DEPTH = 20.0F;
|
||||||
|
|
||||||
|
// Píxels transparents a la base del sprite del jugador (16×32). El
|
||||||
|
// sprite "camina pel mig del passadís en falsa perspectiva", així que
|
||||||
|
// la silueta no toca la vora inferior del rect del sprite — hi queda
|
||||||
|
// un marge transparent. Quan armem el clip rect del forat, restem
|
||||||
|
// aquest offset a la `floor_y` perquè la línia de retall coincideixi
|
||||||
|
// visualment amb la base del tile, no amb la vora del sprite.
|
||||||
|
static constexpr float SPRITE_FLOOR_OFFSET = 3.0F;
|
||||||
|
|
||||||
// Temps restant de stun d'aterratge (ms). Mentre > 0, l'estat és
|
// Temps restant de stun d'aterratge (ms). Mentre > 0, l'estat és
|
||||||
// LANDING_STUN i tot l'input queda bloquejat.
|
// LANDING_STUN i tot l'input queda bloquejat.
|
||||||
float landing_stun_remaining_ms_{0.0F};
|
float landing_stun_remaining_ms_{0.0F};
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Stubs de Player per als binaris `test_entities_smoke` i `test_whip_attack`.
|
||||||
|
//
|
||||||
|
// `Hole` referencia `Player::getHitBox()` i `Player::enterHoleFall()` dins
|
||||||
|
// de `onPlayerContact` (camí no exercit pels tests stand-alone). Linkar
|
||||||
|
// `player.cpp` arrossegaria audio/gamepad/input/screen/cache, una explosió
|
||||||
|
// de dependències que els smokes intenten evitar — mateixa decisió que
|
||||||
|
// `whip_attack_player_stubs.cpp`. Aquests stubs proporcionen els símbols
|
||||||
|
// com a no-op perquè T1/T7 puguin fer `factory.create("hole", ...)`.
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include "game/entities/player.hpp"
|
||||||
|
|
||||||
|
auto Player::getHitBox() const -> SDL_FRect {
|
||||||
|
return SDL_FRect{0.0F, 0.0F, 0.0F, 0.0F};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Player::enterHoleFall(float /*center_x*/, float /*center_y*/, float /*floor_y*/) {
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
#include "game/entities/animation_component.hpp"
|
#include "game/entities/animation_component.hpp"
|
||||||
#include "game/entities/concrete/door.hpp"
|
#include "game/entities/concrete/door.hpp"
|
||||||
#include "game/entities/concrete/floor_trap.hpp"
|
#include "game/entities/concrete/floor_trap.hpp"
|
||||||
|
#include "game/entities/concrete/hole.hpp"
|
||||||
#include "game/entities/concrete/mummy.hpp"
|
#include "game/entities/concrete/mummy.hpp"
|
||||||
#include "game/entities/concrete/rolling_rock.hpp"
|
#include "game/entities/concrete/rolling_rock.hpp"
|
||||||
#include "game/entities/concrete/sarcophagus.hpp"
|
#include "game/entities/concrete/sarcophagus.hpp"
|
||||||
@@ -74,7 +75,7 @@ namespace {
|
|||||||
|
|
||||||
const std::vector<std::string> TYPES = {
|
const std::vector<std::string> TYPES = {
|
||||||
"torch", "sarcophagus", "door", "mummy",
|
"torch", "sarcophagus", "door", "mummy",
|
||||||
"rolling_rock", "wall_trap", "floor_trap",
|
"rolling_rock", "wall_trap", "floor_trap", "hole",
|
||||||
};
|
};
|
||||||
for (const auto& type : TYPES) {
|
for (const auto& type : TYPES) {
|
||||||
fkyaml::node tmpl = EntityLoader::loadTemplate(type);
|
fkyaml::node tmpl = EntityLoader::loadTemplate(type);
|
||||||
@@ -251,7 +252,7 @@ namespace {
|
|||||||
std::cout << "[T7] plantilles YAML\n";
|
std::cout << "[T7] plantilles YAML\n";
|
||||||
const std::vector<std::string> TYPES = {
|
const std::vector<std::string> TYPES = {
|
||||||
"torch", "sarcophagus", "door", "mummy",
|
"torch", "sarcophagus", "door", "mummy",
|
||||||
"rolling_rock", "wall_trap", "floor_trap",
|
"rolling_rock", "wall_trap", "floor_trap", "hole",
|
||||||
};
|
};
|
||||||
for (const auto& type : TYPES) {
|
for (const auto& type : TYPES) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user