feat(hazard): nou hazard Hole — caiguda per forat amb clip rect

This commit is contained in:
JailDesigner committed 2026-04-30 13:32:17 +02:00
1 parent d4593d0686
commit 4da50571eb
17 files changed
+376 -15

No files matched your search

+117 -11
View File
@@ -12,6 +12,7 @@
#include "core/input/gamepad_bindings.hpp"
#include "core/input/gamepad_manager.hpp"
#include "core/input/input_state.hpp"
#include "core/rendering/canvas.hpp"
#include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
@@ -910,6 +911,17 @@ auto Player::integrateJump(float delta_time) -> bool {
if (state_ != State::AIRBORNE) { return false; }
jump_vz_ -= jump_config.gravity_px_per_s2 * 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) {
jump_z_ = 0.0F;
jump_vz_ = 0.0F;
@@ -1075,7 +1087,7 @@ void Player::applyMovement(float delta_time) {
if (state_ == State::GROUNDED) { publishTileEnteredIfChanged(); }
}
void Player::enterDying() {
void Player::enterDying(DeathCause cause) {
// Idempotent. Un segon contacte de momia mentre el jugador ja està
// en DYING no reinicia l'animació ni el comptador. Tampoc disparem
// el cicle si el jugador està fora de joc (EXITED al passar la
@@ -1091,6 +1103,8 @@ void Player::enterDying() {
being_pushed_ = false;
jump_z_ = 0.0F;
jump_vz_ = 0.0F;
falling_through_hole_ = false;
hole_clip_bottom_y_ = 0.0F;
landing_stun_remaining_ms_ = 0.0F;
attacking_remaining_ms_ = 0.0F;
turning_remaining_ms_ = 0.0F;
@@ -1102,7 +1116,18 @@ void Player::enterDying() {
death_animation_complete_ = false;
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
// `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; }
// 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
// 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í:
// això ho fa el GameScene en una passada de sombres a banda.
if (jump_z_ <= 0.0F) {
AnimatedSprite::render();
return;
// això ho fa el GameScene en una passada de sombres a banda. El cas
// `jump_z_ == 0` (a terra) surt per l'early-return per evitar tocar
// `pos_.y` innecessàriament.
// 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_;
AnimatedSprite::render();
pos_.y = SAVED_Y;
if (jump_z_ == 0.0F) {
AnimatedSprite::render();
} 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 {
@@ -1224,6 +1276,10 @@ void Player::renderShadow() const {
// que poden cridar `renderShadow` per a tots els jugadors sense
// discriminar.
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
// (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;
jump_z_ = 0.0F;
jump_vz_ = 0.0F;
falling_through_hole_ = false;
hole_clip_bottom_y_ = 0.0F;
landing_stun_remaining_ms_ = 0.0F;
attacking_remaining_ms_ = 0.0F;
turning_remaining_ms_ = 0.0F;
@@ -1396,6 +1454,54 @@ void Player::disable() {
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) {
// 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