Files
jaildoctors_dilemma/docs/PLAYER_MECHANICS.md
2025-10-31 22:58:37 +01:00

51 KiB

PLAYER MECHANICS - Documentación Completa

Propósito: Este documento define TODAS las reglas, comportamientos y casos edge del sistema de física del jugador en JailDoctor's Dilemma. Es la referencia definitiva para implementar y mantener la clase Player.


⚠️ CAMBIOS IMPORTANTES EN ESTA VERSIÓN

1. Unificación de Variables

  • jump_init_pos_ ha sido ELIMINADO
  • last_grounded_position_ ahora hace doble función:
    1. Guarda la última Y donde el jugador estuvo en tierra firme (para calcular distancia de caída)
    2. Sirve como altura inicial del salto (reemplaza a jump_init_pos_)
  • Justificación: Ambas variables guardaban la misma información (Y al salir de STANDING)

2. Condición de Transición JUMPING → FALLING Corregida

  • Antes (INCORRECTO): y >= jump_init_pos_
  • Ahora (CORRECTO): y > last_grounded_position_
  • Cambio crítico: Usa > (mayor), NO >= (mayor o igual)
  • Razón: Si el jugador vuelve EXACTAMENTE a la altura inicial, debe CONTINUAR en JUMPING

3. Sistema de Sonidos Basado en Distancia Vertical

  • Antes: Sistema frame-based o basado en tiempo (jumping_time_)
  • Ahora: Basado en distancia vertical recorrida
  • Nueva constante: SOUND_DISTANCE_INTERVAL (píxeles entre cada sonido)
  • Nueva variable requerida: float y_prev_ (para detectar cambios de hito)
  • Razón: Delta-time variable hace que los sonidos basados en tiempo sean inconsistentes

4. Sistema de Muerte por Caída Documentado

  • Muerte si caída > 32 píxeles (4 tiles)
  • last_grounded_position_ se guarda AL SALIR de STANDING (no durante el salto)
  • CRÍTICO: En switchBorders() debe resetearse last_grounded_position_ para evitar muerte falsa

5. SpawnData Ampliado

  • Documentado el uso completo del sistema SpawnData
  • Explicado el reseteo de variables en cambio de pantalla
  • Advertencias sobre cálculos de caída entre rooms

1. ESTADOS DEL JUGADOR

El jugador tiene tres estados mutuamente exclusivos:

1.1 STANDING (De pie)

Definición: El jugador está parado sobre una superficie sólida (suelo, rampa, o conveyor belt).

Condiciones de entrada:

  • Aterrizar sobre suelo normal desde el aire (FALLING → STANDING)
  • Aterrizar sobre rampa desde el aire (FALLING → STANDING)
  • Aterrizar sobre conveyor belt desde el aire (FALLING → STANDING)
  • Cambiar de pantalla verticalmente (forzado al cambio de room)

Condiciones de salida:

  • Input de salto + sobre superficie → JUMPING
  • No hay superficie debajo de los pies → FALLING
  • Nunca sale directamente a otro estado sin una de estas condiciones

Comportamiento mientras está en STANDING:

  • Puede moverse horizontalmente (izquierda/derecha)
  • Puede saltar
  • Se pega a las rampas al moverse lateralmente
  • Desciende rampas incrementando Y cuando detecta rampa hacia abajo
  • Puede ser movido por conveyor belts (auto_movement)
  • NO puede cambiar de dirección en el aire

Variables afectadas:

  • vy_ = 0 (siempre, mientras está STANDING)
  • vx_ = controlada por input o conveyor belt
  • last_grounded_position_ = Y actual (para calcular distancia de caída)
  • jumping_time_ = 0

1.2 JUMPING (Saltando)

Definición: El jugador está en el aire debido a un salto iniciado por el jugador.

Condiciones de entrada:

  • Estado STANDING + input de salto + (isOnFloor() || isOnAutoSurface())
  • Nunca puede iniciar salto si no está sobre una superficie

Condiciones de salida:

  • Golpea el techo (colisión superior) → FALLING
  • Y actual > Y inicial del salto (SUPERA la altura de inicio) → FALLING
    • ⚠️ IMPORTANTE: Se usa > (mayor que), NO >= (mayor o igual)
    • Si el jugador vuelve EXACTAMENTE a la altura inicial, debe CONTINUAR en JUMPING
    • Solo cuando la SUPERA (desciende más allá) cambia a FALLING
  • Nunca aterriza directamente en STANDING (siempre pasa por FALLING primero)

Comportamiento mientras está en JUMPING:

  • Gravedad se aplica constantemente (vy_ aumenta cada frame)
  • Mantiene la velocidad horizontal con la que saltó (vx_ no cambia)
  • Puede atravesar rampas SI tiene vx_ != 0
  • NO puede cambiar la dirección horizontal (vx_ es fijo)
  • NO se pega a rampas durante el salto
  • ⚠️ SI salta recto (vx_ == 0), se pega a rampas al descender

Variables afectadas:

  • vy_ = JUMP_VELOCITY al inicio (-80 px/s), luego aumenta por gravedad
  • vx_ = valor que tenía al iniciar el salto (no cambia durante JUMPING)
  • jump_init_pos_ = Y en el momento de inicio del salto
  • jumping_time_ = se incrementa cada frame (usado para sonidos)
  • last_grounded_position_ = SE GUARDA AL SALIR DE STANDING (no cambia durante el salto)

Sonidos - Sistema Basado en Distancia Vertical:

El jugador reproduce sonidos progresivos durante el salto basándose en la distancia vertical recorrida, no en el tiempo.

// Cálculo del índice de sonido
const int SOUND_INDEX = static_cast<int>(std::abs(y_ - jump_init_pos_) / SOUND_DISTANCE_INTERVAL);
const int PREVIOUS_INDEX = static_cast<int>(std::abs(y_prev_ - jump_init_pos_) / SOUND_DISTANCE_INTERVAL);

// Solo reproduce cuando cambia el índice
if (SOUND_INDEX != PREVIOUS_INDEX && SOUND_INDEX < jumping_sound_.size()) {
    JA_PlaySound(jumping_sound_[SOUND_INDEX]);
}

Constantes de sonido:

  • SOUND_DISTANCE_INTERVAL = Distancia en píxeles entre cada sonido (ej: 3-4 píxeles)
  • jumping_sound_ = Vector con 24 sonidos (jump1.wav a jump24.wav)

Justificación: Con delta-time variable, usar tiempo produce sonidos inconsistentes a diferentes framerates. La distancia vertical es constante independientemente del framerate.


1.3 FALLING (Cayendo)

Definición: El jugador está cayendo debido a gravedad (sin estar en un salto activo).

Condiciones de entrada:

  • Estado STANDING + no hay superficie debajo → FALLING
  • Estado JUMPING + Y > jump_init_pos_ → FALLING (nota: > no >=)
  • Estado JUMPING + colisión con techo → FALLING

Condiciones de salida:

  • Colisión con suelo normal → STANDING
  • Colisión con conveyor belt → STANDING
  • Colisión con rampa → STANDING
  • Nunca sale a JUMPING (no se puede saltar en el aire)

Comportamiento mientras está en FALLING:

  • Cae a velocidad constante (MAX_VY)
  • Se pega a TODAS las rampas (independientemente de vx_)
  • NO puede cambiar dirección horizontal (vx_ = 0)
  • NO aplica gravedad (velocidad es constante MAX_VY)

Variables afectadas:

  • vy_ = MAX_VY (80 px/s, velocidad máxima de caída)
  • vx_ = 0 (no puede moverse horizontalmente)
  • auto_movement_ = false (desactiva conveyor belts)
  • jumping_time_ = 0
  • last_grounded_position_ = NO CAMBIA (se mantiene el valor guardado al salir de STANDING)

Sistema de Muerte por Caída Excesiva:

El jugador muere si cae desde una altura mayor a MAX_FALLING_HEIGHT = 32 píxeles (4 tiles).

Algoritmo:

  1. Al salir de STANDING (transición a JUMPING o FALLING):

    last_grounded_position_ = static_cast<int>(y_);  // Guarda Y actual
    
  2. Durante JUMPING/FALLING:

    • last_grounded_position_ NO cambia
    • Representa la última Y donde estuvo en tierra firme
  3. Al aterrizar (transición a STANDING):

    const int FALL_DISTANCE = static_cast<int>(y_) - last_grounded_position_;
    if (FALL_DISTANCE > MAX_FALLING_HEIGHT) {
        is_alive_ = false;  // Muere por caída
    }
    

⚠️ Caso Especial - Cambio de Pantalla:

Al hacer switchBorders(), el jugador se teleporta al extremo opuesto. La diferencia de Y podría ser ~192 píxeles (altura total de pantalla), causando muerte falsa.

Solución: Al cambiar de pantalla, se resetea last_grounded_position_:

void Player::switchBorders() {
    // ... teleportar jugador ...
    setState(State::STANDING);
    last_grounded_position_ = static_cast<int>(y_);  // ← RESETEAR
}

Sonidos - Sistema Basado en Distancia Vertical:

Similar al sistema de JUMPING, pero usando los sonidos de caída:

// Cálculo del índice de sonido
const int SOUND_INDEX = static_cast<int>((y_ - last_grounded_position_) / SOUND_DISTANCE_INTERVAL);
const int PREVIOUS_INDEX = static_cast<int>((y_prev_ - last_grounded_position_) / SOUND_DISTANCE_INTERVAL);

// Solo reproduce cuando cambia el índice
if (SOUND_INDEX != PREVIOUS_INDEX && SOUND_INDEX < falling_sound_.size()) {
    JA_PlaySound(falling_sound_[SOUND_INDEX]);
}

Constantes de sonido:

  • falling_sound_ = Vector con 14 sonidos (jump11.wav a jump24.wav - reutiliza los últimos sonidos del salto)

2. SISTEMA DE VELOCIDADES

2.1 Velocidad Horizontal (vx_)

Unidades: píxeles/segundo

Valores posibles:

  • -HORIZONTAL_VELOCITY (-40 px/s): Movimiento a la izquierda
  • 0: Sin movimiento horizontal
  • +HORIZONTAL_VELOCITY (+40 px/s): Movimiento a la derecha
  • +/- HORIZONTAL_VELOCITY * direction: En conveyor belts (direction = ±1)

Reglas de cambio por estado:

Estado ¿Puede cambiar vx_? ¿Cómo se determina?
STANDING Input del jugador (izq/der) O conveyor belt
JUMPING No Se fija al valor que tenía al iniciar el salto
FALLING No Siempre 0

Casos especiales:

  • Conveyor belt activo: vx_ = HORIZONTAL_VELOCITY * room_->getAutoSurfaceDirection()
  • Sin input en STANDING: vx_ = 0 (se detiene)
  • Flip del sprite: Si vx_ < 0 → SDL_FLIP_HORIZONTAL, si vx_ > 0 → SDL_FLIP_NONE

2.2 Velocidad Vertical (vy_)

Unidades: píxeles/segundo

Valores posibles:

  • 0: Sin movimiento vertical (solo en STANDING)
  • JUMP_VELOCITY (-80 px/s): Inicio del salto (negativo = hacia arriba)
  • MAX_VY (+80 px/s): Velocidad máxima de caída (positivo = hacia abajo)
  • Cualquier valor entre JUMP_VELOCITY y MAX_VY durante JUMPING (por gravedad)

Reglas de cambio por estado:

Estado Valor de vy_ ¿Cambia?
STANDING 0 No, siempre 0
JUMPING JUMP_VELOCITY → MAX_VY Sí, por gravedad
FALLING MAX_VY No, constante

Gravedad:

  • Solo se aplica en JUMPING
  • Fórmula: vy_ += GRAVITY_FORCE * delta_time
  • GRAVITY_FORCE = 155.6 px/s²
  • vy_ se limita a MAX_VY: vy_ = std::min(vy_, MAX_VY)

3. TIPOS DE SUPERFICIES

3.1 Suelo Normal (Top Surfaces)

Definición: Superficies sólidas horizontales sobre las que el jugador puede caminar.

Detección:

  • room_->checkTopSurfaces(SDL_FRect*) - Para aterrizaje (devuelve Y o -1)
  • room_->checkTopSurfaces(SDL_FPoint*) - Para verificar si punto está en suelo (bool)

Comportamiento:

  • Jugador camina normalmente
  • Puede saltar
  • No afecta vx_ automáticamente
  • Desactiva auto_movement si estaba activo

Interacción por estado:

  • STANDING: Camina sobre ella
  • JUMPING: Puede pasar a través si está ascendiendo, aterriza si está descendiendo
  • FALLING: Aterriza al tocarla (transición a STANDING)

3.2 Conveyor Belts (Auto Surfaces)

Definición: Superficies que mueven al jugador automáticamente en una dirección.

Detección:

  • room_->checkAutoSurfaces(SDL_FRect*) - Para aterrizaje (devuelve Y o -1)
  • room_->checkAutoSurfaces(SDL_FPoint*) - Para verificar si punto está en conveyor (bool)

Propiedades:

  • Tienen una dirección: room_->getAutoSurfaceDirection() devuelve +1 (derecha) o -1 (izquierda)
  • Se comportan como suelo normal + movimiento automático

Activación de auto_movement:

auto_movement se activa cuando:
  - Jugador está STANDING
  - Está sobre una conveyor belt (isOnAutoSurface())
  - NO está presionando ninguna tecla de dirección
  - NO está sobre suelo normal simultáneamente

Desactivación de auto_movement:

auto_movement se desactiva cuando:
  - Jugador salta (transición a JUMPING)
  - Jugador cae (transición a FALLING)
  - Jugador aterriza en suelo normal
  - Ambos pies del jugador dejan la conveyor belt

Comportamiento mientras auto_movement = true:

  • vx_ = HORIZONTAL_VELOCITY * direction (sobrescribe input)
  • Jugador NO puede cambiar dirección con teclas
  • Jugador SÍ puede saltar (mantiene la inercia de la belt)

Casos especiales:

  • Caer sobre conveyor belt: El jugador aterriza normalmente, pero auto_movement NO se activa hasta que deje de pulsar direcciones
  • Rampa + Conveyor belt: ¿Posible? (verificar en el juego)

3.3 Rampas (Slopes)

Definición: Superficies diagonales que el jugador puede subir, bajar o atravesar según el estado.

3.3.1 Rampa Izquierda (Left Slope)

Características:

Ascendente cuando te mueves a la IZQUIERDA
Descendente cuando te mueves a la DERECHA

Visual:
     ####
    ####
   ####
  ####
 ####
####

Detección:

  • room_->checkLeftSlopes(LineVertical*) - Devuelve Y de la rampa o -1
  • room_->checkLeftSlopes(SDL_FPoint*) - Devuelve bool

Comportamiento:

  • Moverse a la izquierda: El jugador sube la rampa (Y disminuye)
  • Moverse a la derecha: El jugador baja la rampa (Y aumenta)

3.3.2 Rampa Derecha (Right Slope)

Características:

Ascendente cuando te mueves a la DERECHA
Descendente cuando te mueves a la IZQUIERDA

Visual:
####
 ####
  ####
   ####
    ####
     ####

Detección:

  • room_->checkRightSlopes(LineVertical*) - Devuelve Y de la rampa o -1
  • room_->checkRightSlopes(SDL_FPoint*) - Devuelve bool

Comportamiento:

  • Moverse a la derecha: El jugador sube la rampa (Y disminuye)
  • Moverse a la izquierda: El jugador baja la rampa (Y aumenta)

3.3.3 Reglas de Interacción con Rampas

REGLA CRÍTICA: Las rampas se comportan diferente según el estado del jugador.

Estado vx_ Comportamiento Justificación
STANDING Cualquiera Se PEGA a la rampa Camina sobre ella
JUMPING != 0 ATRAVIESA la rampa Salta sobre ella en movimiento
JUMPING == 0 Se PEGA a la rampa Salto recto no tiene momento horizontal
FALLING Cualquiera Se PEGA a la rampa Aterriza en ella

Detección de "Pegar a la Rampa":

  1. Contacto Lateral (durante movimiento horizontal):

    • Se usa una línea vertical en el borde izquierdo/derecho del jugador
    • Solo se comprueban los 2 píxeles inferiores: y1 = y + HEIGHT - 2, y2 = y + HEIGHT - 1
    • Si hay rampa, reposiciona Y: y = rampa_y - HEIGHT
  2. Contacto Inferior (durante caída):

    • Se usan los pies del jugador (LineVertical de arriba a abajo del rectángulo)
    • LEFT_SIDE = {x, y, y + HEIGHT - 1}
    • RIGHT_SIDE = {x + WIDTH - 1, y, y + HEIGHT - 1}
    • Comprueba ambas rampas (left y right), toma el máximo Y
    • Si hay rampa, reposiciona Y: y = rampa_y - HEIGHT
  3. Descenso de Rampa:

    • Se detecta con isOnDownSlope()
    • Comprueba los puntos under_feet_ + 1 píxel adicional hacia abajo
    • Si hay rampa descendente, incrementa Y: y += 1

Casos Edge:

  • Rampa + Suelo normal simultáneamente: El suelo normal tiene prioridad (se usa std::max para elegir la Y más alta)
  • Rampa izquierda + Rampa derecha en el mismo punto: Se toma el máximo Y

4. SISTEMA DE COLISIONES

4.1 Puntos de Colisión del Jugador

El jugador usa múltiples conjuntos de puntos para detectar colisiones:

4.1.1 Rectángulo Principal (collider_box_)

SDL_FRect collider_box_ = {x_, y_, WIDTH, HEIGHT};
  • Tamaño: 8x16 píxeles
  • Uso: Colisión con enemigos y objetos (no usado para colisión con tiles)

4.1.2 Puntos de Colisión (collider_points_)

std::array<SDL_FPoint, 8> collider_points_;

Disposición: 4 esquinas de cada mitad vertical del jugador (8x8 píxeles cada mitad)

Píxel (0,0) → collider_points_[0]     collider_points_[1] ← (7,0)
              collider_points_[3]     collider_points_[2]
              ─────────────────────────────────────────
              collider_points_[4]     collider_points_[5]
Píxel (0,15)→ collider_points_[7]     collider_points_[6] ← (7,15)

Uso: Detectar si el jugador está tocando tiles que matan (KILL tiles)

Cálculo:

collider_points_[0] = {x, y};
collider_points_[1] = {x + 7, y};
collider_points_[2] = {x + 7, y + 7};
collider_points_[3] = {x, y + 7};
collider_points_[4] = {x, y + 8};
collider_points_[5] = {x + 7, y + 8};
collider_points_[6] = {x + 7, y + 15};
collider_points_[7] = {x, y + 15};

4.1.3 Pies (feet_)

std::array<SDL_FPoint, 2> feet_;

Posición: Esquinas inferiores del jugador (Y = y + HEIGHT - 1)

feet_[0] = {x, y + 15}        feet_[1] = {x + 7, y + 15}

Uso: Representan el límite inferior del jugador (usado para verificar si está parado)

4.1.4 Bajo los Pies (under_feet_)

std::array<SDL_FPoint, 2> under_feet_;

Posición: 1 píxel debajo de los pies (Y = y + HEIGHT)

under_feet_[0] = {x, y + 16}        under_feet_[1] = {x + 7, y + 16}

Uso: Detectar QUÉ superficie está debajo del jugador (suelo, conveyor belt, rampa)


4.2 Rectángulos de Proyección

Concepto: En lugar de comprobar colisiones en la posición actual, se crea un rectángulo que cubre el espacio entre la posición actual y la posición futura. Esto previene "tunneling" (atravesar paredes a alta velocidad).

4.2.1 Proyección Horizontal Izquierda

const float DISPLACEMENT = vx_ * delta_time;  // Negativo para izquierda
SDL_FRect proj = {
    .x = x_ + DISPLACEMENT,  // Posición futura
    .y = y_,
    .w = std::ceil(std::fabs(DISPLACEMENT)),  // Ancho = distancia recorrida
    .h = HEIGHT
};

Comprobación: room_->checkRightSurfaces(&proj) - Busca paredes a la derecha del proyección (porque nos movemos a la izquierda)

4.2.2 Proyección Horizontal Derecha

const float DISPLACEMENT = vx_ * delta_time;  // Positivo para derecha
SDL_FRect proj = {
    .x = x_ + WIDTH,  // Empieza desde el borde derecho del jugador
    .y = y_,
    .w = std::ceil(DISPLACEMENT),  // Ancho = distancia recorrida
    .h = HEIGHT
};

Comprobación: room_->checkLeftSurfaces(&proj) - Busca paredes a la izquierda de la proyección (porque nos movemos a la derecha)

4.2.3 Proyección Vertical Arriba

const float DISPLACEMENT = vy_ * delta_time;  // Negativo para arriba
SDL_FRect proj = {
    .x = x_,
    .y = y_ + DISPLACEMENT,  // Posición futura (más arriba)
    .w = WIDTH,
    .h = std::ceil(std::fabs(DISPLACEMENT))  // Alto = distancia recorrida
};

Comprobación: room_->checkBottomSurfaces(&proj) - Busca techos (superficies inferiores de tiles)

4.2.4 Proyección Vertical Abajo

const float DISPLACEMENT = vy_ * delta_time;  // Positivo para abajo
SDL_FRect proj = {
    .x = x_,
    .y = y_ + HEIGHT,  // Empieza desde el borde inferior del jugador
    .w = WIDTH,
    .h = std::ceil(DISPLACEMENT)  // Alto = distancia recorrida
};

Comprobación:

  • room_->checkTopSurfaces(&proj) - Busca suelos
  • room_->checkAutoSurfaces(&proj) - Busca conveyor belts
  • Se toma el máximo: std::max(checkTopSurfaces, checkAutoSurfaces)

4.3 Orden de Resolución de Colisiones

Movimiento Horizontal:

  1. Crear proyección horizontal
  2. Comprobar colisión con muros (checkLeftSurfaces o checkRightSurfaces)
  3. Si hay colisión → reposicionar en el punto de colisión
  4. Si NO hay colisión → aplicar desplazamiento
  5. Comprobar rampas laterales (solo si STANDING o FALLING)
  6. Comprobar rampas descendentes (isOnDownSlope)

Movimiento Vertical Arriba:

  1. Crear proyección vertical
  2. Comprobar colisión con techos (checkBottomSurfaces)
  3. Si hay colisión → reposicionar + cambiar a FALLING
  4. Si NO hay colisión → aplicar desplazamiento

Movimiento Vertical Abajo:

  1. Crear proyección vertical
  2. Comprobar colisión con suelos y conveyor belts (checkTopSurfaces + checkAutoSurfaces)
  3. Si hay colisión → reposicionar + cambiar a STANDING
  4. Si NO hay colisión → comprobar rampas (checkLeftSlopes + checkRightSlopes)
  5. Si hay rampa Y no estamos atravesando → reposicionar en rampa + cambiar a STANDING
  6. Si NO hay nada → aplicar desplazamiento

5. MÉTODOS DE DETECCIÓN (Room)

5.1 Métodos con SDL_FRect (para movimiento)

Estos métodos toman un rectángulo de proyección y devuelven la posición de colisión o -1 si no hay colisión.

Método Parámetro Retorna Propósito
checkTopSurfaces(SDL_FRect*) Proyección abajo Y o -1 Detectar suelo al caer
checkBottomSurfaces(SDL_FRect*) Proyección arriba Y o -1 Detectar techo al subir
checkLeftSurfaces(SDL_FRect*) Proyección derecha X o -1 Detectar pared a la derecha
checkRightSurfaces(SDL_FRect*) Proyección izquierda X o -1 Detectar pared a la izquierda
checkAutoSurfaces(SDL_FRect*) Proyección abajo Y o -1 Detectar conveyor belt al caer

Uso típico:

const float POS = room_->checkTopSurfaces(&proj);
if (POS > -1) {
    // Hay colisión en Y = POS
    y_ = POS - HEIGHT;  // Reposicionar
}

5.2 Métodos con SDL_FPoint (para verificación)

Estos métodos toman un punto y devuelven bool indicando si ese punto está en la superficie.

Método Parámetro Retorna Propósito
checkTopSurfaces(SDL_FPoint*) Punto bool ¿Hay suelo en este punto?
checkAutoSurfaces(SDL_FPoint*) Punto bool ¿Hay conveyor belt en este punto?
checkLeftSlopes(SDL_FPoint*) Punto bool ¿Hay rampa izquierda en este punto?
checkRightSlopes(SDL_FPoint*) Punto bool ¿Hay rampa derecha en este punto?

Uso típico:

bool on_floor = false;
for (auto f : under_feet_) {
    on_floor |= room_->checkTopSurfaces(&f);
}

5.3 Métodos con LineVertical (para rampas)

Estos métodos toman una línea vertical y devuelven la Y de la rampa o -1 si no hay rampa.

Método Parámetro Retorna Propósito
checkLeftSlopes(LineVertical*) Línea vertical Y o -1 Detectar rampa izquierda en contacto lateral
checkRightSlopes(LineVertical*) Línea vertical Y o -1 Detectar rampa derecha en contacto lateral

LineVertical:

struct LineVertical {
    int x;   // Posición X de la línea
    int y1;  // Y inicial
    int y2;  // Y final
};

Uso típico:

const LineVertical SIDE = {
    .x = static_cast<int>(x_),
    .y1 = static_cast<int>(y_) + HEIGHT - 2,
    .y2 = static_cast<int>(y_) + HEIGHT - 1
};
const int SLOPE_Y = room_->checkLeftSlopes(&SIDE);
if (SLOPE_Y > -1) {
    y_ = SLOPE_Y - HEIGHT;  // Pegar a la rampa
}

5.4 Otros Métodos de Room

Método Propósito
getTile(SDL_FPoint) Devuelve el tipo de tile en ese punto (Room::Tile::KILL, etc.)
getAutoSurfaceDirection() Devuelve +1 o -1 (dirección de conveyor belts)

6. MÉTODOS AUXILIARES DEL PLAYER

6.1 isOnFloor()

Propósito: Determinar si el jugador tiene una superficie sólida debajo de los pies.

Algoritmo:

auto Player::isOnFloor() -> bool {
    updateFeet();  // Calcula under_feet_ positions

    bool on_floor = false;
    bool on_slope_l = false;
    bool on_slope_r = false;

    // Comprueba suelo normal y conveyor belts
    for (auto f : under_feet_) {
        on_floor |= room_->checkTopSurfaces(&f);
        on_floor |= room_->checkAutoSurfaces(&f);
    }

    // Comprueba rampas
    on_slope_l = room_->checkLeftSlopes(&under_feet_[0]);
    on_slope_r = room_->checkRightSlopes(&under_feet_[1]);

    return on_floor || on_slope_l || on_slope_r;
}

Retorna: true si hay suelo, conveyor belt, o rampa bajo los pies.

Cuándo usar: Para verificar si el jugador debe caer (transición STANDING → FALLING)


6.2 isOnAutoSurface()

Propósito: Determinar si el jugador está sobre una conveyor belt.

Algoritmo:

auto Player::isOnAutoSurface() -> bool {
    updateFeet();

    bool on_auto_surface = false;
    for (auto f : under_feet_) {
        on_auto_surface |= room_->checkAutoSurfaces(&f);
    }

    return on_auto_surface;
}

Retorna: true si alguno de los puntos under_feet_ está en una conveyor belt.

Cuándo usar: Para activar auto_movement o para permitir saltos.


6.3 isOnDownSlope()

Propósito: Determinar si el jugador está sobre una rampa descendente (necesita incrementar Y para seguir la rampa).

Algoritmo:

auto Player::isOnDownSlope() -> bool {
    updateFeet();

    // Mira 1 píxel MÁS abajo que under_feet_
    SDL_FPoint foot0 = under_feet_[0];
    SDL_FPoint foot1 = under_feet_[1];
    foot0.y += 1.0f;
    foot1.y += 1.0f;

    bool on_slope = false;
    on_slope |= room_->checkLeftSlopes(&foot0);
    on_slope |= room_->checkRightSlopes(&foot1);

    return on_slope;
}

Retorna: true si hay una rampa 1 píxel por debajo de los pies (indica descenso).

Cuándo usar: Durante movimiento horizontal en STANDING, para pegar al jugador a la rampa al descender.


7. CASOS EDGE DOCUMENTADOS

7.1 Saltar Recto sobre Rampa

Situación: Jugador está en rampa, salta sin moverse horizontalmente.

Input: Salto (sin izquierda/derecha)

Comportamiento esperado:

  1. Estado STANDING → JUMPING
  2. vx_ = 0 (no hay movimiento horizontal)
  3. Jugador sube por el salto
  4. Al descender, detecta rampa con vx_ == 0
  5. Se pega a la rampa (NO la atraviesa)
  6. Aterriza en STANDING

Justificación: Un salto recto no tiene momento horizontal, por lo que el jugador debe aterrizar donde saltó.


7.2 Saltar con Movimiento Horizontal sobre Rampa

Situación: Jugador está en rampa, salta mientras se mueve.

Input: Salto + izquierda/derecha

Comportamiento esperado:

  1. Estado STANDING → JUMPING
  2. vx_ = ±HORIZONTAL_VELOCITY (mantiene dirección)
  3. Jugador sube por el salto
  4. Durante JUMPING, vx_ != 0 → atraviesa la rampa
  5. Continúa el arco del salto
  6. Aterriza más allá de la rampa (si hay suelo)

Justificación: El momento horizontal permite atravesar la rampa.


7.3 Caer desde Altura sobre Rampa (Sin Salto Previo)

Situación: Jugador camina fuera de un borde y cae sobre una rampa.

Input: Ninguno (caída por gravedad)

Comportamiento esperado:

  1. Estado STANDING → FALLING (no hay suelo debajo)
  2. vx_ = 0 (FALLING siempre tiene vx_ = 0)
  3. Jugador cae a MAX_VY
  4. Detecta rampa durante caída
  5. Se pega a la rampa (vx_ == 0)
  6. Aterriza en STANDING

Justificación: Una caída sin salto previo no tiene momento horizontal, se pega a rampas.


7.4 Cambiar de Pantalla Verticalmente

Situación: Jugador alcanza el borde superior o inferior de la pantalla.

Input: Movimiento que lleva al jugador al borde

Comportamiento completo:

  1. Detección: checkBorders() detecta is_on_border_ = true y establece qué borde (border_)

  2. Teleportación: switchBorders() es llamado:

    void Player::switchBorders() {
        switch (border_) {
            case Room::Border::TOP:
                y_ = PLAY_AREA_BOTTOM - HEIGHT - TILE_SIZE;
                break;
            case Room::Border::BOTTOM:
                y_ = PLAY_AREA_TOP;
                break;
            case Room::Border::LEFT:
                x_ = PLAY_AREA_RIGHT - WIDTH;
                break;
            case Room::Border::RIGHT:
                x_ = PLAY_AREA_LEFT;
                break;
        }
    
        // CRÍTICO: Resetear estado y variables de tracking
        setState(State::STANDING);
        last_grounded_position_ = static_cast<int>(y_);  // ← Evita muerte falsa
        is_on_border_ = false;
        placeSprite();
    }
    
  3. Reseteo de tracking:

    • Estado forzado a STANDING (independientemente del estado anterior)
    • last_grounded_position_ se resetea a la nueva Y
    • Justificación: Sin este reseteo, la diferencia de Y (~192 píxeles) causaría muerte falsa por "caída excesiva"
  4. Frame siguiente:

    • Si no hay suelo en la nueva posición, shouldFall() detecta la ausencia de suelo
    • Transición STANDING → FALLING
    • El jugador cae normalmente

SpawnData - Sistema de Guardado/Restauración:

El sistema usa SpawnData para guardar/restaurar el estado completo del jugador:

struct SpawnData {
    float x, y;              // Posición
    float vx, vy;            // Velocidades
    int jump_init_pos;       // Altura inicial del salto
    State state;             // Estado actual
    SDL_FlipMode flip;       // Orientación del sprite
};

Usos de SpawnData:

  1. Cambio de pantalla: Guardar estado antes de cambiar de room
  2. Muerte/Respawn: Restaurar jugador al último checkpoint
  3. Save/Load: Persistir estado del jugador entre sesiones

Métodos:

  • getSpawnParams() - Devuelve SpawnData actual
  • applySpawnValues(const SpawnData&) - Restaura estado desde SpawnData

⚠️ Importante: Al restaurar desde SpawnData después de un cambio de room, también se debe resetear last_grounded_position_ para evitar cálculos incorrectos de distancia de caída entre rooms diferentes.


7.5 Saltar desde Conveyor Belt

Situación: Jugador está en conveyor belt (auto_movement = true) y salta.

Input: Salto

Comportamiento esperado:

  1. Estado STANDING → JUMPING
  2. vx_ = HORIZONTAL_VELOCITY * direction (mantiene inercia de la belt)
  3. auto_movement = false (desactivado en setState(JUMPING))
  4. Jugador salta en la dirección de la belt
  5. Durante JUMPING, vx_ != 0 (mantiene el momento)
  6. Al aterrizar, auto_movement se mantiene false hasta que el jugador deje de pulsar direcciones

Justificación: El jugador hereda el momento de la conveyor belt.


7.6 Conveyor Belt en Rampa

Situación: ¿Es posible que exista una conveyor belt sobre una rampa?

A verificar en el juego:

  • Si existe, ¿cómo se comporta?
  • ¿Tiene prioridad la rampa o la conveyor belt?
  • ¿El jugador es movido horizontalmente mientras sube/baja la rampa?

Hipótesis: No debería existir en el diseño de niveles, pero si existe, se comportaría como conveyor belt normal (prioridad de suelo sobre rampa).


7.7 Múltiples Superficies Simultáneas

Situación: Los dos puntos under_feet_ detectan superficies diferentes.

Ejemplos:

  • under_feet_[0] en suelo normal, under_feet_[1] en conveyor belt
  • under_feet_[0] en rampa, under_feet_[1] en suelo normal
  • under_feet_[0] en conveyor belt, under_feet_[1] en rampa

Comportamiento:

  • isOnFloor() usa OR lógico → devuelve true si ANY pie está en superficie
  • Para aterrizaje, se usa std::max(Y_suelo, Y_conveyor, Y_rampa) → jugador queda en la superficie más alta
  • auto_movement se activa solo si está en conveyor belt Y NO en suelo normal

Justificación: El jugador se para en la superficie más alta disponible.


8. TABLA DE DECISIONES PARA COLISIONES

Esta tabla define EXACTAMENTE qué hacer en cada caso:

8.1 Movimiento Vertical Hacia Abajo (moveVerticalDown)

Estado Actual vx_ Superficie Detectada Acción Nuevo Estado
STANDING Cualquiera Ninguna (aire) Continuar cayendo FALLING
STANDING Cualquiera Suelo (No debería ocurrir, ya está STANDING) STANDING
JUMPING != 0 Suelo Aterrizar (y = suelo_y - HEIGHT) STANDING
JUMPING != 0 Rampa ATRAVESAR (y += displacement) JUMPING
JUMPING == 0 Suelo Aterrizar (y = suelo_y - HEIGHT) STANDING
JUMPING == 0 Rampa PEGAR (y = rampa_y - HEIGHT) STANDING
FALLING Cualquiera Suelo Aterrizar (y = suelo_y - HEIGHT) STANDING
FALLING Cualquiera Rampa PEGAR (y = rampa_y - HEIGHT) STANDING

8.2 Movimiento Horizontal (moveHorizontal)

Estado Actual Superficie Lateral Rampa Bajo Pies Acción
STANDING Pared No Detener en pared (x = pared_x ± width)
STANDING Libre Rampa lateral Subir rampa (y = rampa_y - HEIGHT)
STANDING Libre Rampa descendente Descender rampa (y += 1)
JUMPING Pared No Detener en pared (x = pared_x ± width)
JUMPING Libre Rampa IGNORAR (no pegar)
FALLING Pared No Detener en pared (x = pared_x ± width)
FALLING Libre Rampa lateral Subir rampa (y = rampa_y - HEIGHT)

8.3 Activación de auto_movement

Condiciones auto_movement
Estado = STANDING + isOnAutoSurface() + !isOnFloor() + input = ninguno Activa (true)
Estado = JUMPING Desactiva (false)
Estado = FALLING Desactiva (false)
Estado = STANDING + isOnFloor() (suelo normal) Desactiva (false)
Estado = STANDING + input != ninguno Mantiene (no cambia)

9. ORDEN DE EJECUCIÓN ÓPTIMO

Basándose en todas las reglas documentadas, el orden de ejecución debe ser:

9.1 Opción A: checkState Integrado (Versión Funcional)

update(delta_time) {
    if (is_paused_) return;

    checkInput()  // Modifica vx_ directamente, detecta want_to_jump

    move(delta_time) {
        applyGravity()  // Solo si JUMPING

        checkState(delta_time)  // ← CRÍTICO: Estado + velocidades se actualizan JUNTOS

        moveHorizontal()  // Usa vx_ actualizado
        moveVertical()    // Usa vy_ actualizado, comprueba estado actual

        updateColliderGeometry()
    }

    animate()
    checkBorders()
    checkJumpEnd()
    checkKillingTiles()
    setColor()
}

Ventajas:

  • Estado y velocidades siempre sincronizados
  • moveVertical ve el estado correcto
  • Probado funciona (commit 7cd596a)

Desventajas:

  • checkState hace demasiadas cosas (cambiar estado, modificar vx/vy, reproducir sonidos)
  • Mezcla responsabilidades

9.2 Opción B: State-First con updateState al FINAL

update(delta_time) {
    if (is_paused_) return;

    checkInput()  // Captura intent flags

    applyGravity()  // Solo si JUMPING
    updateVelocity()  // Basado en estado ACTUAL

    move(delta_time) {
        moveHorizontal()  // Mueve en X
        moveVertical()    // Mueve en Y, puede cambiar estado (JUMPING→FALLING, FALLING→STANDING)
        updateColliderGeometry()
    }

    updateState(delta_time)  // ← DESPUÉS del movimiento, confirma/ajusta el estado

    animate()
    checkBorders()
    checkJumpEnd()
    checkKillingTiles()
    setColor()
}

Ventajas:

  • Separación clara de responsabilidades
  • updateState solo gestiona transiciones de estado
  • moveVertical puede cambiar estado localmente (aterrizaje)

Desventajas:

  • ⚠️ Requiere que moveVertical pueda cambiar estado directamente (setState)
  • ⚠️ Dos lugares cambian estado: moveVertical (aterrizaje) y updateState (salto, caída)

9.3 Opción C: Híbrido (updateState DENTRO de move, entre movimientos)

update(delta_time) {
    if (is_paused_) return;

    checkInput()  // Captura intent flags

    applyGravity()  // Solo si JUMPING
    updateVelocity()  // Basado en estado actual

    move(delta_time) {
        moveHorizontal()  // Mueve en X

        updateState(delta_time)  // ← ENTRE horizontal y vertical

        moveVertical()    // Mueve en Y con estado actualizado
        updateColliderGeometry()
    }

    animate()
    checkBorders()
    checkJumpEnd()
    checkKillingTiles()
    setColor()
}

Ventajas:

  • Estado se actualiza después de movimiento horizontal (conveyor belts OK)
  • Estado se actualiza antes de movimiento vertical (necesario para rampas)

Desventajas:

  • NO FUNCIONA porque updateVelocity ya corrió antes de updateState
  • Cuando updateState cambia JUMPING→FALLING, vx_ no se actualiza a 0
  • Este es el problema actual que estamos teniendo

9.4 Recomendación Final

La mejor opción es la Opción A (checkState integrado) con mejoras de organización:

update(delta_time) {
    if (is_paused_) return;

    // 1. Captura input
    checkInput()  // Lee teclas, establece want_to_jump_, want_to_move_left/right_

    // 2. Movimiento con estado integrado
    move(delta_time) {
        applyGravity()              // Aplica gravedad si JUMPING
        updateStateAndVelocity()    // ← Nuevo: combina checkState + updateVelocity
        moveHorizontal()            // Movimiento X con vx_ correcto
        moveVertical()              // Movimiento Y con vy_ correcto
        updateColliderGeometry()
    }

    // 3. Finalización
    animate()
    checkBorders()
    checkJumpEnd()
    checkKillingTiles()
    setColor()
}

updateStateAndVelocity() haría:

  1. Comprobar transiciones de estado basadas en física + input
  2. Actualizar state_ y previous_state_
  3. Actualizar vx_ y vy_ basándose en el nuevo estado
  4. Actualizar auto_movement_ basándose en el nuevo estado + input
  5. Reproducir sonidos si corresponde

Esto garantiza que estado y velocidades están SIEMPRE sincronizados.


10. VARIABLES IMPORTANTES Y CUÁNDO CAMBIAN

10.1 state_ (Estado actual)

Tipo: Player::State (STANDING, JUMPING, FALLING)

Cuándo cambia:

De → A Condición Dónde
STANDING → JUMPING want_to_jump_ && (isOnFloor() || isOnAutoSurface()) updateStateAndVelocity()
STANDING → FALLING !isOnFloor() && !isOnAutoSurface() && !isOnDownSlope() updateStateAndVelocity()
JUMPING → FALLING y > last_grounded_position_ && vy > 0 (nota: > no >=) updateStateAndVelocity()
JUMPING → FALLING Colisión con techo moveVerticalUp()
FALLING → STANDING Colisión con suelo/rampa moveVerticalDown()

10.2 vx_ (Velocidad horizontal)

Tipo: float (píxeles/segundo)

Cuándo cambia:

Estado Condición Valor Dónde
STANDING !auto_movement_ && want_to_move_left_ -HORIZONTAL_VELOCITY updateStateAndVelocity()
STANDING !auto_movement_ && want_to_move_right_ +HORIZONTAL_VELOCITY updateStateAndVelocity()
STANDING !auto_movement_ && sin input 0 updateStateAndVelocity()
STANDING auto_movement_ HORIZONTAL_VELOCITY * direction updateStateAndVelocity()
JUMPING - (no cambia, mantiene valor inicial) -
FALLING Transición a FALLING 0 setState(FALLING)

10.3 vy_ (Velocidad vertical)

Tipo: float (píxeles/segundo)

Cuándo cambia:

Estado Evento Valor Dónde
STANDING Siempre 0 updateStateAndVelocity()
JUMPING Inicio del salto JUMP_VELOCITY setState(JUMPING)
JUMPING Cada frame vy_ += GRAVITY * dt applyGravity()
FALLING Transición a FALLING MAX_VY setState(FALLING)

10.4 auto_movement_ (Conveyor belt activo)

Tipo: bool

Cuándo cambia:

De → A Condición Dónde
false → true STANDING + isOnAutoSurface() + sin input + !isOnFloor() updateStateAndVelocity()
true → false Transición a JUMPING setState(JUMPING)
true → false Transición a FALLING setState(FALLING)
true → false Aterrizaje en suelo normal moveVerticalDown()

11. CONSTANTES FÍSICAS

// Dimensiones del jugador
WIDTH = 8;                              // píxeles
HEIGHT = 16;                            // píxeles
MAX_FALLING_HEIGHT = TILE_SIZE * 4;    // 32 píxeles (muere si cae más)
TILE_SIZE = 8;                          // píxeles por tile

// Velocidades (píxeles/segundo)
HORIZONTAL_VELOCITY = 40.0F;            // Velocidad de caminar/correr
MAX_VY = 80.0F;                         // Velocidad máxima de caída
JUMP_VELOCITY = -80.0F;                 // Velocidad inicial del salto (negativo = arriba)
GRAVITY_FORCE = 155.6F;                 // Aceleración de gravedad (px/s²)

// Sonidos - Sistema basado en distancia vertical
SOUND_DISTANCE_INTERVAL = 3.0F;         // Píxeles entre cada sonido (ajustar según diseño)
// Vectores de sonidos:
// - jumping_sound_: 24 sonidos (jump1.wav a jump24.wav)
// - falling_sound_: 14 sonidos (jump11.wav a jump24.wav)

12. PSEUDOCÓDIGO COMPLETO (Versión Definitiva)

// ==================== UPDATE ====================
void Player::update(float delta_time) {
    if (is_paused_) return;

    checkInput();
    move(delta_time);
    animate(delta_time);
    checkBorders();
    checkJumpEnd();
    checkKillingTiles();
    setColor();
}

// ==================== CHECK INPUT ====================
void Player::checkInput() {
    want_to_jump_ = Input::get()->checkInput(InputAction::JUMP);
    want_to_move_left_ = Input::get()->checkInput(InputAction::LEFT);
    want_to_move_right_ = Input::get()->checkInput(InputAction::RIGHT);
}

// ==================== MOVE ====================
void Player::move(float delta_time) {
    applyGravity(delta_time);              // Gravedad solo si JUMPING
    updateStateAndVelocity(delta_time);    // Actualiza estado + vx/vy juntos
    moveHorizontal(delta_time);            // Movimiento X
    moveVertical(delta_time);              // Movimiento Y (puede cambiar estado)
    updateColliderGeometry();
}

// ==================== APPLY GRAVITY ====================
void Player::applyGravity(float delta_time) {
    if (state_ == State::JUMPING) {
        vy_ += GRAVITY_FORCE * delta_time;
        vy_ = std::min(vy_, MAX_VY);
    }
}

// ==================== UPDATE STATE AND VELOCITY ====================
void Player::updateStateAndVelocity(float delta_time) {
    previous_state_ = state_;

    switch (state_) {
        case State::STANDING:
            // Muerte por caída
            if (previous_state_ == State::FALLING) {
                int fall_distance = static_cast<int>(y_) - last_grounded_position_;
                if (fall_distance > MAX_FALLING_HEIGHT) {
                    is_alive_ = false;
                }
            }

            // Actualizar posición de tierra
            // NOTA: last_grounded_position_ hace doble función:
            //   1. Guarda la última Y en tierra (para calcular distancia de caída)
            //   2. Sirve como jump_init_pos_ (altura inicial del salto)
            last_grounded_position_ = static_cast<int>(y_);

            // vy_ siempre 0 en STANDING
            vy_ = 0.0F;

            // Transición a FALLING si no hay suelo
            if (!isOnFloor() && !isOnAutoSurface() && !isOnDownSlope()) {
                state_ = State::FALLING;
                vx_ = 0.0F;
                vy_ = MAX_VY;
                auto_movement_ = false;
                jumping_time_ = 0.0F;
                return;  // Salir para aplicar FALLING
            }

            // Transición a JUMPING si se pulsa salto
            if (want_to_jump_ && (isOnFloor() || isOnAutoSurface())) {
                state_ = State::JUMPING;
                // last_grounded_position_ ya está actualizado (líneas arriba)
                // Se usa como altura inicial del salto
                vy_ = JUMP_VELOCITY;
                // vx_ se mantiene (hereda momento)
                return;  // Salir para mantener vx_ actual
            }

            // Actualizar vx_ según input o auto_movement
            if (!auto_movement_) {
                if (want_to_move_left_) {
                    vx_ = -HORIZONTAL_VELOCITY;
                    sprite_->setFlip(SDL_FLIP_HORIZONTAL);
                } else if (want_to_move_right_) {
                    vx_ = HORIZONTAL_VELOCITY;
                    sprite_->setFlip(SDL_FLIP_NONE);
                } else {
                    vx_ = 0.0F;
                    // Activar auto_movement si está en conveyor belt
                    if (isOnAutoSurface() && !isOnFloor()) {
                        auto_movement_ = true;
                    }
                }
            } else {
                // Auto movement activo: conveyor belt controla vx_
                vx_ = HORIZONTAL_VELOCITY * room_->getAutoSurfaceDirection();
                sprite_->setFlip(vx_ > 0.0F ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL);
            }
            break;

        case State::JUMPING:
            // Reproducir sonidos basados en distancia vertical
            playJumpSound();

            // Transición a FALLING si SUPERA altura inicial
            // ⚠️ IMPORTANTE: Usar > (mayor), NO >= (mayor o igual)
            if (static_cast<int>(y_) > last_grounded_position_ && vy_ > 0.0F) {
                state_ = State::FALLING;
                vx_ = 0.0F;
                vy_ = MAX_VY;
                auto_movement_ = false;
                // last_grounded_position_ NO cambia (se mantiene desde el inicio del salto)
            }

            // vx_ no cambia durante JUMPING (mantiene momento)
            // vy_ ya fue actualizado por applyGravity()
            // last_grounded_position_ NO cambia durante el salto
            break;

        case State::FALLING:
            // Reproducir sonido de caída
            playFallSound();

            // vx_ = 0, vy_ = MAX_VY (ya establecidos en transición)
            // No cambian durante FALLING
            break;
    }
}

// ==================== MOVE HORIZONTAL ====================
void Player::moveHorizontal(float delta_time) {
    if (vx_ == 0.0F) return;  // Sin movimiento horizontal

    int direction = (vx_ < 0.0F) ? -1 : 1;
    float displacement = vx_ * delta_time;

    // Crear proyección
    SDL_FRect proj;
    if (direction < 0) {
        proj = {x_ + displacement, y_, std::ceil(std::fabs(displacement)), HEIGHT};
    } else {
        proj = {x_ + WIDTH, y_, std::ceil(displacement), HEIGHT};
    }

    // Comprobar colisión con muros
    int wall_pos = (direction < 0) ? room_->checkRightSurfaces(&proj)
                                    : room_->checkLeftSurfaces(&proj);

    if (wall_pos == -1) {
        // No hay colisión: mover
        x_ += displacement;
    } else {
        // Hay colisión: detener en muro
        x_ = (direction < 0) ? wall_pos + 1 : wall_pos - WIDTH;
    }

    // Manejar rampas solo si no está JUMPING
    if (state_ != State::JUMPING) {
        handleSlopeMovement(direction);
    }
}

// ==================== HANDLE SLOPE MOVEMENT ====================
void Player::handleSlopeMovement(int direction) {
    // Si está descendiendo rampa, pegar al jugador
    if (isOnDownSlope()) {
        y_ += 1;
        return;
    }

    // Comprobar rampa lateral (contacto lateral = subir rampa)
    int side_x = (direction < 0) ? static_cast<int>(x_)
                                  : static_cast<int>(x_) + WIDTH - 1;
    LineVertical side = {side_x, static_cast<int>(y_) + HEIGHT - 2,
                                  static_cast<int>(y_) + HEIGHT - 1};

    int slope_y = (direction < 0) ? room_->checkLeftSlopes(&side)
                                   : room_->checkRightSlopes(&side);

    if (slope_y > -1) {
        y_ = slope_y - HEIGHT;  // Subir a la rampa
    }
}

// ==================== MOVE VERTICAL ====================
void Player::moveVertical(float delta_time) {
    if (vy_ < 0.0F) {
        moveVerticalUp(delta_time);
    } else if (vy_ > 0.0F) {
        moveVerticalDown(delta_time);
    }
}

// ==================== MOVE VERTICAL UP ====================
void Player::moveVerticalUp(float delta_time) {
    float displacement = vy_ * delta_time;
    SDL_FRect proj = {x_, y_ + displacement, WIDTH, std::ceil(std::fabs(displacement))};

    int ceiling_pos = room_->checkBottomSurfaces(&proj);

    if (ceiling_pos == -1) {
        // No hay colisión: mover
        y_ += displacement;
    } else {
        // Hay colisión con techo: detener y cambiar a FALLING
        y_ = ceiling_pos + 1;
        state_ = State::FALLING;
        vx_ = 0.0F;
        vy_ = MAX_VY;
        auto_movement_ = false;
        jumping_time_ = 0.0F;
    }
}

// ==================== MOVE VERTICAL DOWN ====================
void Player::moveVerticalDown(float delta_time) {
    float displacement = vy_ * delta_time;
    SDL_FRect proj = {x_, y_ + HEIGHT, WIDTH, std::ceil(displacement)};

    // Comprobar suelo y conveyor belts
    float floor_pos = std::max(room_->checkTopSurfaces(&proj),
                                room_->checkAutoSurfaces(&proj));

    if (floor_pos > -1) {
        // Hay suelo: aterrizar
        y_ = floor_pos - HEIGHT;
        state_ = State::STANDING;
        auto_movement_ = false;
        return;
    }

    // No hay suelo: comprobar rampas
    // REGLA: Se pega a rampas SI:
    //   - NO está JUMPING, O
    //   - Está JUMPING pero vx_ == 0 (salto recto)

    if (state_ != State::JUMPING || vx_ == 0.0F) {
        SDL_FRect rect = getRect();
        LineVertical left_side = {rect.x, rect.y, rect.y + rect.h - 1};
        LineVertical right_side = {rect.x + rect.w - 1, rect.y, rect.y + rect.h - 1};

        float slope_pos = std::max(room_->checkRightSlopes(&right_side),
                                    room_->checkLeftSlopes(&left_side));

        if (slope_pos > -1) {
            // Hay rampa: aterrizar
            y_ = slope_pos - HEIGHT;
            state_ = State::STANDING;
            auto_movement_ = false;
            return;
        }
    }

    // No hay nada: continuar cayendo
    y_ += displacement;
}

// ==================== PLAY JUMP SOUND ====================
void Player::playJumpSound() {
    // Sistema basado en distancia vertical recorrida
    const float DISTANCE_FROM_START = std::abs(y_ - static_cast<float>(last_grounded_position_));
    const int SOUND_INDEX = static_cast<int>(DISTANCE_FROM_START / SOUND_DISTANCE_INTERVAL);

    // Calcular índice previo (frame anterior)
    const float PREV_DISTANCE = std::abs(y_prev_ - static_cast<float>(last_grounded_position_));
    const int PREVIOUS_INDEX = static_cast<int>(PREV_DISTANCE / SOUND_DISTANCE_INTERVAL);

    // Solo reproduce cuando cambia de índice (nuevo hito alcanzado)
    if (SOUND_INDEX != PREVIOUS_INDEX && SOUND_INDEX < static_cast<int>(jumping_sound_.size())) {
        JA_PlaySound(jumping_sound_[SOUND_INDEX]);
    }

    // NOTA: Necesitamos guardar y_ del frame anterior
    // Agregar variable: float y_prev_ = 0.0F;
    // Actualizar en move(): y_prev_ = y_;
}

// ==================== PLAY FALL SOUND ====================
void Player::playFallSound() {
    // Sistema basado en distancia vertical caída
    const float DISTANCE_FALLEN = y_ - static_cast<float>(last_grounded_position_);
    const int SOUND_INDEX = static_cast<int>(DISTANCE_FALLEN / SOUND_DISTANCE_INTERVAL);

    // Calcular índice previo (frame anterior)
    const float PREV_DISTANCE = y_prev_ - static_cast<float>(last_grounded_position_);
    const int PREVIOUS_INDEX = static_cast<int>(PREV_DISTANCE / SOUND_DISTANCE_INTERVAL);

    // Solo reproduce cuando cambia de índice
    if (SOUND_INDEX != PREVIOUS_INDEX && SOUND_INDEX < static_cast<int>(falling_sound_.size())) {
        JA_PlaySound(falling_sound_[SOUND_INDEX]);
    }
}

FIN DEL DOCUMENTO

Este documento define TODAS las reglas mecánicas del Player. Cualquier implementación debe seguir estas especificaciones exactamente. Si surge una ambigüedad o un caso no documentado, debe agregarse a este documento primero antes de implementarlo.

Versión: 1.0 Fecha: 2025-10-30 Autor: Documentación basada en análisis del código existente y PLAYER_RULES.md