From 82ccf65ce1e4baaa6d00a13e830fcef1bfd106e0 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 2 May 2026 14:30:09 +0200 Subject: [PATCH] =?UTF-8?q?fix(player):=20no=20snap=20a=20lane=20si=20pica?= =?UTF-8?q?=20contra=20mur=20extern;=20corridor=20nom=C3=A9s=20si=20porta?= =?UTF-8?q?=20oberta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/maps/p1/r1.yaml | 6 +-- source/game/entities/player.cpp | 93 ++++++++++++++++++++++++--------- source/game/entities/player.hpp | 18 +++++-- 3 files changed, 82 insertions(+), 35 deletions(-) diff --git a/data/maps/p1/r1.yaml b/data/maps/p1/r1.yaml index 10e028f..c96e67b 100644 --- a/data/maps/p1/r1.yaml +++ b/data/maps/p1/r1.yaml @@ -49,7 +49,7 @@ geometry: # 16 px centrals (just aquesta columna) són el "forat" del mur trasser per # on el jugador surt de la sala. Les 2 columnes flanquejants segueixen sent # mur visible. El spawn dels jugadors també s'alinea amb aquesta columna. - door_col: 14 + door_col: 11 data: - [11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13] @@ -128,8 +128,6 @@ entities: sarc_position: [2, 4] - type: sarcophagus sarc_position: [3, 4] - - type: mummy - position: [10, 7] - type: hole position: [4, 7] - type: hole @@ -140,6 +138,4 @@ entities: position: [15, 5] - type: hole position: [8, 5] - - type: mummy - position: [8, 9] - type: door diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index f1864f2..21f6800 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -1617,24 +1617,13 @@ auto Player::hitBoxFromBase(const SDL_FRect& base) -> SDL_FRect { }; } -void Player::snapToLane( +void Player::snapTowardsLane( float delta_time, - float base, - float pitch, - int max_idx, + float target_lane, float (Player::*get_pos)() const, void (Player::*set_pos)(float)) { - // Lanes en pos_axis: base + i·pitch per a i ∈ [0, max_idx]. Correspon a - // la caixa de col·lisió ocupant exactament un passadís perpendicular. const auto THRESHOLD = static_cast(collision_size) / 2.0F; - - const auto IDX = std::clamp( - static_cast(std::round(((this->*get_pos)() - base) / pitch)), - 0, - max_idx); - const float LANE = base + (static_cast(IDX) * pitch); - const float DELTA = LANE - (this->*get_pos)(); - + const float DELTA = target_lane - (this->*get_pos)(); if (std::abs(DELTA) <= 0.0F || std::abs(DELTA) > THRESHOLD) { return; } const float FULL_STEP = std::min(std::abs(DELTA), SPEED_PX_PER_S * delta_time); @@ -1719,20 +1708,72 @@ auto Player::tryPushSnapBlockers( return true; } +auto Player::wouldHitOuterWall(float dx, float dy) const -> bool { + SDL_FRect probe = getBaseBox(); + probe.x += dx; + probe.y += dy; + if (Map::insidePlayfield(probe)) { return false; } + // Excepció: corridor de la porta OBERTA quan l'usuari prem cap + // amunt (dy < 0). No demanem que la probe ja estigui dins del + // corridor — encara no ho està, perquè el snap és justament qui + // ha d'aliniar-la. En lloc d'això comprovem si la base box està + // prou prop del centre del corridor com per a que el snap + // (mateix THRESHOLD = collision_size/2) puga completar-lo. + if (Map::door_open && dy < 0.0F && map_.door_corridor_w > 0) { + const float CORRIDOR_TARGET = static_cast(map_.door_corridor_x) + (static_cast(map_.door_corridor_w - collision_size) / 2.0F); + const auto THRESHOLD = static_cast(collision_size) / 2.0F; + if (std::abs(CORRIDOR_TARGET - probe.x) <= THRESHOLD) { return false; } + } + return true; +} + void Player::snapToHorizontalLane(float delta_time) { - snapToLane(delta_time, - static_cast(map_.playfield_y - collision_y_offset), - static_cast(map_.pitch_y), - map_.grid_rows, - &Player::getPosY, - &Player::setPosY); + // Si el moviment principal del frame (eix X) està blocat per un mur + // extern, no fem snap perpendicular: l'usuari està picant contra el + // mur i no té sentit arrossegar-lo a un carril. Step probe d'1 px. + if (intended_vx_ != 0.0F && wouldHitOuterWall(std::copysign(1.0F, intended_vx_), 0.0F)) { + return; + } + + const auto BASE_Y = static_cast(map_.playfield_y - collision_y_offset); + const auto PITCH_Y = static_cast(map_.pitch_y); + const int IDX = std::clamp( + static_cast(std::round((getPosY() - BASE_Y) / PITCH_Y)), + 0, + map_.grid_rows); + const float LANE_REG = BASE_Y + (static_cast(IDX) * PITCH_Y); + snapTowardsLane(delta_time, LANE_REG, &Player::getPosY, &Player::setPosY); } void Player::snapToVerticalLane(float delta_time) { - snapToLane(delta_time, - static_cast(map_.playfield_x - collision_x_offset), - static_cast(map_.pitch_x), - map_.grid_cols, - &Player::getPosX, - &Player::setPosX); + // Mateixa idea que la versió horitzontal: si el moviment principal + // (eix Y) està blocat per un mur extern, no aliniem en X. La + // excepció del corridor de la porta hi aplica naturalment dins + // `wouldHitOuterWall` (només quan és OBERT). + if (intended_vy_ != 0.0F && wouldHitOuterWall(0.0F, std::copysign(1.0F, intended_vy_))) { + return; + } + + // Candidat 1: lane regular del laberint. + const auto BASE_X = static_cast(map_.playfield_x - collision_x_offset); + const auto PITCH_X = static_cast(map_.pitch_x); + const int IDX = std::clamp( + static_cast(std::round((getPosX() - BASE_X) / PITCH_X)), + 0, + map_.grid_cols); + const float LANE_REG = BASE_X + (static_cast(IDX) * PITCH_X); + + // Candidat 2: corridor de la porta. NOMÉS si la porta està oberta — + // si està tancada no té sentit arrossegar el jugador cap a un + // forat per on no pot passar. + float best_lane = LANE_REG; + if (Map::door_open && map_.door_corridor_w > 0) { + const float CORRIDOR_CENTER = static_cast(map_.door_corridor_x) + (static_cast(map_.door_corridor_w - collision_size) / 2.0F); + const float LANE_DOOR = CORRIDOR_CENTER - static_cast(collision_x_offset); + const float DELTA_REG = LANE_REG - getPosX(); + const float DELTA_DOOR = LANE_DOOR - getPosX(); + if (std::abs(DELTA_DOOR) < std::abs(DELTA_REG)) { best_lane = LANE_DOOR; } + } + + snapTowardsLane(delta_time, best_lane, &Player::getPosX, &Player::setPosX); } diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 150b94d..b47290f 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -436,14 +436,24 @@ class Player : public AnimatedSprite { // attack/recover), el sufix del cardinal (`_north`/`_south`/`_east`/ // `_west`). [[nodiscard]] auto animationName(bool moving) const -> std::string; - void snapToLane( + // Aplica el snap cap a una lane concreta amb la mecànica de fases + // 1/2 + push de blockers. Els wrappers (`snapToHorizontalLane` / + // `snapToVerticalLane`) decideixen la lane diana (grid regular, o + // corridor de la porta com a candidata especial). + void snapTowardsLane( float delta_time, - float base, - float pitch, - int max_idx, + float target_lane, float (Player::*get_pos)() const, void (Player::*set_pos)(float)); + // Cert si un step (dx, dy) trauria la base box fora del playfield + // (mur extern). Excepció: si el moviment és cap amunt (dy<0), la + // porta està OBERTA i la box probe queda dins del rang X del + // corridor, no és bloqueig — el jugador hi pot sortir. S'usa per + // suprimir el snap a lane quan el jugador no pot avançar perquè + // pica contra un mur exterior. + [[nodiscard]] auto wouldHitOuterWall(float dx, float dy) const -> bool; + // Quan el snap lateral trobaria un altre jugador al carril objectiu, // l'empentem el mateix step en el mateix eix. Així `this` pot entrar // al passadís encara que l'altre estiga estacionat al mitj — mateix