diff --git a/CLAUDE.md b/CLAUDE.md index 3b519bc..72b331d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,6 +81,16 @@ Se han renombrado las referencias de `JailDoctor's Dilemma` → `Projecte 2026` 6. `syncSpriteAndCollider()` + `animate()` + `handleBorders()` - **~20 métodos eliminados** del Player antiguo. Slopes gestionadas por tile (slope_tile_x/y/type) en vez de puntero a LineDiagonal. - **Sistema antiguo de superficies preservado** en CollisionMap (no eliminado), simplemente ya no usado por Player. +- **Slopes — escalera diagonal de tiles:** + - Las slopes de 45° se pintan como escalera en el collision tilemap: cada tile una fila arriba/abajo y una columna al lado. + - `checkSlopeBelow`: escanea la fila de los pies Y la de arriba (la slope entry siempre está una fila arriba del suelo). + - `followSlope`: busca el siguiente tile de slope en la fila actual Y la de abajo (para descenso en escalera). + - `exitSlope`: comprueba suelo en `foot_y` y `foot_y+1` (boundary de fila al salir por abajo) y snapea al borde del tile. +- **Drop-through — sin flags, puramente posicional:** + - Al pulsar DOWN sobre slope/plataforma: `y_ += 1` y ON_AIR. No hay flags `dropping_through_`. + - `checkFloor` para PASSABLE: solo aterriza si `foot_y_current <= tile_top` (pies estaban por encima). + - `checkFloor` para slopes: solo aterriza si `foot_y_current <= slope_y` (pies por encima de la superficie). + - `isInsideAnySlope()`: si algún pie está por debajo de la superficie de cualquier slope que solape, bloquea TODOS los aterrizajes en slopes. Esto asegura que al hacer drop o saltar desde abajo, el jugador atraviesa toda la escalera de slopes sin quedarse pegado. - **Pendiente:** tiles 5 (kill) y 6 (conveyor) no soportados aún en el nuevo motor. ### Otros diff --git a/data/room/03.yaml b/data/room/03.yaml index 83f7d63..4698cd0 100644 --- a/data/room/03.yaml +++ b/data/room/03.yaml @@ -32,16 +32,16 @@ tilemap: - [24, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 504, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - [24, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 504, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - [24, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 504, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [24, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 504, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [24, 26, -1, -1, -1, -1, -1, -1, -1, 6, 7, 7, 7, 7, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] + - [24, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 504, -1, -1, 307, 307, 307, 307, -1, -1, -1, -1, -1, -1, -1, 24, 26] + - [24, 26, -1, -1, -1, -1, -1, -1, -1, 6, 7, 7, 7, 7, 7, 8, -1, -1, -1, -1, -1, 305, 305, -1, -1, -1, -1, -1, -1, -1, 24, 26] - [24, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [24, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] + - [24, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 2, -1, -1, 307, 307, -1, -1, -1, -1, -1, -1, -1, 24, 26] - [48, 50, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 48, 50, 329, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 1, 2, -1, -1, -1, -1, -1, 24, 26] - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 25, 25, 26, -1, -1, -1, -1, -1, 24, 26] - - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 53, 25, 25, 51, 1, 1, 1, 1, 1, 53, 51] + - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 53, 25, 25, 51, 555, 555, 555, 555, 555, 53, 51] # Mapa de colisiones (0 = vacio, 1 = solido) collision: - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] @@ -55,16 +55,16 @@ tilemap: - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - - [1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] + - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 1] + - [1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] + - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1] - - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5, 5, 5, 1, 1] # Enemigos en esta habitación enemies: diff --git a/data/tilesets/collision.gif b/data/tilesets/collision.gif index b12a903..ada22c0 100644 Binary files a/data/tilesets/collision.gif and b/data/tilesets/collision.gif differ diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 6376a77..5272cad 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -246,10 +246,14 @@ void Player::moveHorizontal(float delta_time) { } } +// Ajusta Y del jugador para seguir la superficie de la slope mientras camina. +// Si el pie sale del tile actual, busca el siguiente tile de slope en la fila +// actual y la inferior (las slopes en escalera bajan una fila por tile). +// Si no encuentra slope, llama a exitSlope(). void Player::followSlope() { const auto& tc = room_->getTileCollider(); - // Seleccionar pie según tipo de slope + // SLOPE_L (\): pie izquierdo. SLOPE_R (/): pie derecho. float foot_x = (slope_type_ == TileCollider::Tile::SLOPE_L) ? x_ : x_ + WIDTH - 1; // Calcular Y en la slope actual @@ -261,63 +265,56 @@ void Player::followSlope() { int foot_tile_y = static_cast(y_ + HEIGHT) / Tile::SIZE; if (foot_tile_x != slope_tile_x_ || foot_tile_y != slope_tile_y_) { - // Mirar si el nuevo tile también es slope - auto new_tile = tc.getTileAt(foot_tile_x, foot_tile_y); - if (new_tile == TileCollider::Tile::SLOPE_L || new_tile == TileCollider::Tile::SLOPE_R) { - slope_tile_x_ = foot_tile_x; - slope_tile_y_ = foot_tile_y; - slope_type_ = new_tile; - surface_y = tc.getSlopeY(slope_tile_x_, slope_tile_y_, foot_x); - y_ = surface_y - HEIGHT; - } else { - exitSlope(); + // Buscar slope en el tile calculado y en el de abajo (la escalera de slopes + // siempre tiene el siguiente tile una fila arriba o abajo) + for (int row = foot_tile_y; row <= foot_tile_y + 1; ++row) { + auto new_tile = tc.getTileAt(foot_tile_x, row); + if (new_tile == TileCollider::Tile::SLOPE_L || new_tile == TileCollider::Tile::SLOPE_R) { + slope_tile_x_ = foot_tile_x; + slope_tile_y_ = row; + slope_type_ = new_tile; + surface_y = tc.getSlopeY(slope_tile_x_, slope_tile_y_, foot_x); + y_ = surface_y - HEIGHT; + return; + } } + exitSlope(); } } +// El jugador ha salido del tile de slope sin encontrar otra slope adyacente. +// Comprueba si hay suelo debajo (foot_y y foot_y+1 para cubrir el boundary exacto +// entre filas cuando se sale por el extremo inferior de la slope). +// Si hay suelo, snapea al borde del tile. Si no, empieza a caer. void Player::exitSlope() { const auto& tc = room_->getTileCollider(); - - // Corrección de 1px al salir por arriba de la slope - y_ += 1.0F; float foot_y = y_ + HEIGHT; - if (tc.hasGroundBelow(x_, foot_y, WIDTH)) { - transitionToState(State::ON_GROUND); - } else { - vy_ = 0.0F; - transitionToState(State::ON_AIR); + // Comprobar suelo en la fila actual y la siguiente (al salir por abajo de una slope, + // los pies pueden estar en el último pixel de la fila, justo antes del suelo) + for (int check = 0; check <= 1; ++check) { + float check_y = foot_y + check; + if (tc.hasGroundBelow(x_, check_y, WIDTH)) { + int row = static_cast(check_y) / Tile::SIZE; + y_ = static_cast(row * Tile::SIZE) - HEIGHT; + transitionToState(State::ON_GROUND); + return; + } } + + vy_ = 0.0F; + transitionToState(State::ON_AIR); } +// Detecta si el jugador ha pisado una slope caminando desde suelo plano. +// Las slopes en escalera están una fila arriba del suelo, así que checkSlopeBelow +// también mira la fila superior. void Player::detectSlopeEntry() { const auto& tc = room_->getTileCollider(); float foot_y = y_ + HEIGHT; auto slope = tc.checkSlopeBelow(x_, foot_y, WIDTH); - - // LOG: siempre mostrar lo que ve detectSlopeEntry - int left_col = static_cast(x_) / Tile::SIZE; - int right_col = static_cast(x_ + WIDTH - 1) / Tile::SIZE; - int row = static_cast(foot_y) / Tile::SIZE; - auto tile_l = tc.getTileAt(left_col, row); - auto tile_r = tc.getTileAt(right_col, row); - SDL_Log("detectSlopeEntry: foot_y=%.1f row=%d cols=[%d,%d] tiles=[%d,%d] slope.on=%d", - foot_y, - row, - left_col, - right_col, - static_cast(tile_l), - static_cast(tile_r), - slope.on_slope ? 1 : 0); - if (slope.on_slope) { - SDL_Log(" -> ENTERING slope type=%d tile=(%d,%d) surface_y=%.1f new_y=%.1f", - static_cast(slope.type), - slope.tile_x, - slope.tile_y, - slope.surface_y, - slope.surface_y - HEIGHT); y_ = slope.surface_y - HEIGHT; slope_tile_x_ = slope.tile_x; slope_tile_y_ = slope.tile_y; @@ -392,9 +389,7 @@ void Player::checkFalling() { // ON_GROUND: comprobar si sigue habiendo suelo float foot_y = y_ + HEIGHT; - bool ground = tc.hasGroundBelow(x_, foot_y, WIDTH); - if (!ground) { - SDL_Log("checkFalling: NO ground at foot_y=%.1f x=%.1f -> ON_AIR", foot_y, x_); + if (!tc.hasGroundBelow(x_, foot_y, WIDTH)) { vy_ = 0.0F; transitionToState(State::ON_AIR); } diff --git a/source/game/gameplay/tile_collider.cpp b/source/game/gameplay/tile_collider.cpp index aa146a7..8ac870d 100644 --- a/source/game/gameplay/tile_collider.cpp +++ b/source/game/gameplay/tile_collider.cpp @@ -25,6 +25,10 @@ auto TileCollider::isSolid(int tile_x, int tile_y) const -> bool { return getTileAt(tile_x, tile_y) == Tile::WALL; } +// Calcula la Y de la superficie de una slope en un pixel X concreto. +// Las slopes son de 45° y ocupan un tile de 8x8: +// SLOPE_L (\): alto a la izquierda, bajo a la derecha. surface = bottom - (7 - x_in_tile) +// SLOPE_R (/): alto a la derecha, bajo a la izquierda. surface = bottom - x_in_tile auto TileCollider::getSlopeY(int tile_x, int tile_y, float px) const -> float { float tile_bottom = static_cast((tile_y + 1) * TS - 1); float x_in_tile = px - static_cast(tile_x * TS); @@ -32,11 +36,9 @@ auto TileCollider::getSlopeY(int tile_x, int tile_y, float px) const -> float { auto tile = getTileAt(tile_x, tile_y); if (tile == Tile::SLOPE_L) { - // \ descendente de izquierda a derecha return tile_bottom - (static_cast(TS - 1) - x_in_tile); } if (tile == Tile::SLOPE_R) { - // / descendente de derecha a izquierda return tile_bottom - x_in_tile; } return tile_bottom; @@ -88,12 +90,21 @@ auto TileCollider::checkCeiling(float x, float y, float w) const -> float { // --- Colisión con suelo (landing) --- +// Busca suelo entre foot_y_current y foot_y_new (rango de caída del frame). +// WALL: siempre bloquea. +// PASSABLE: solo si los pies estaban por encima del borde superior del tile. +// SLOPE: solo si los pies estaban por encima de la superficie Y el jugador no está +// parcialmente dentro de otra slope (evita aterrizar al hacer drop-through +// o al saltar a través de una slope desde abajo). auto TileCollider::checkFloor(float x, float foot_y_current, float w, float foot_y_new) const -> FloorHit { int start_row = toTile(static_cast(foot_y_current)); int end_row = toTile(static_cast(foot_y_new)); int left_col = toTile(static_cast(x)); int right_col = toTile(static_cast(x + w - 1)); + // Si algún pie está por debajo de la superficie de algún slope → bloquear aterrizaje en slopes + bool block_slope_landing = isInsideAnySlope(x, foot_y_current, w); + FloorHit best; for (int row = start_row; row <= end_row; ++row) { @@ -109,10 +120,11 @@ auto TileCollider::checkFloor(float x, float foot_y_current, float w, float foot if (foot_y_current <= tile_top) { floor_y = tile_top; } - } else if (tile == Tile::SLOPE_L || tile == Tile::SLOPE_R) { + } else if (!block_slope_landing && (tile == Tile::SLOPE_L || tile == Tile::SLOPE_R)) { float check_x = (tile == Tile::SLOPE_L) ? x : x + w - 1; float slope_y = getSlopeY(col, row, check_x); - if (foot_y_new >= slope_y && foot_y_current <= slope_y + TS) { + // Solo aterrizar si los pies estaban por encima de la superficie + if (foot_y_new >= slope_y && foot_y_current <= slope_y) { floor_y = slope_y; } } @@ -149,15 +161,43 @@ auto TileCollider::hasGroundBelow(float x, float foot_y, float w) const -> bool return false; } +// --- Comprueba si el jugador está parcialmente dentro de algún slope --- + +// Devuelve true si algún pie del jugador está POR DEBAJO de la superficie de algún +// tile de slope que solape. Esto indica que el jugador está "dentro" de una slope +// (por ejemplo, tras hacer drop-through o al saltar desde abajo). +// Cuando esto ocurre, checkFloor bloquea el aterrizaje en slopes para evitar que +// el jugador se quede pegado encima de una slope que está atravesando. +auto TileCollider::isInsideAnySlope(float x, float foot_y, float w) const -> bool { + int foot_row = toTile(static_cast(foot_y)); + int left_col = toTile(static_cast(x)); + int right_col = toTile(static_cast(x + w - 1)); + + for (int row = foot_row - 1; row <= foot_row; ++row) { + for (int col = left_col; col <= right_col; ++col) { + auto tile = getTileAt(col, row); + if (tile == Tile::SLOPE_L || tile == Tile::SLOPE_R) { + float check_x = (tile == Tile::SLOPE_L) ? x : x + w - 1; + float slope_y = getSlopeY(col, row, check_x); + if (foot_y > slope_y) { + return true; + } + } + } + } + return false; +} + // --- Detección de slope debajo (transición ground→slope) --- +// Busca una slope directamente debajo del jugador (para transición ground→slope). +// Escanea la fila de los pies Y la fila superior: las slopes en escalera siempre +// tienen el tile de entrada una fila arriba del suelo desde el que se accede. auto TileCollider::checkSlopeBelow(float x, float foot_y, float w) const -> SlopeInfo { int foot_row = toTile(static_cast(foot_y)); int left_col = toTile(static_cast(x)); int right_col = toTile(static_cast(x + w - 1)); - // Comprobar la fila de los pies Y la fila de arriba - // (la slope entry puede estar un tile arriba del nivel de la plataforma) for (int row = foot_row - 1; row <= foot_row; ++row) { for (int col = left_col; col <= right_col; ++col) { auto tile = getTileAt(col, row); diff --git a/source/game/gameplay/tile_collider.hpp b/source/game/gameplay/tile_collider.hpp index af48bec..8096013 100644 --- a/source/game/gameplay/tile_collider.hpp +++ b/source/game/gameplay/tile_collider.hpp @@ -44,6 +44,10 @@ class TileCollider { [[nodiscard]] auto hasGroundBelow(float x, float foot_y, float w) const -> bool; [[nodiscard]] auto checkSlopeBelow(float x, float foot_y, float w) const -> SlopeInfo; + // Devuelve true si el jugador está parcialmente dentro de algún tile de slope + // (algún pie está por debajo de la superficie de un slope que solapa) + [[nodiscard]] auto isInsideAnySlope(float x, float foot_y, float w) const -> bool; + private: static constexpr int TS = ::Tile::SIZE; static constexpr int MW = ::Map::WIDTH;