diff --git a/PLAYER_MECHANICS.md b/PLAYER_MECHANICS.md new file mode 100644 index 0000000..f69136e --- /dev/null +++ b/PLAYER_MECHANICS.md @@ -0,0 +1,1529 @@ +# 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. + +```cpp +// Cálculo del índice de sonido +const int SOUND_INDEX = static_cast(std::abs(y_ - jump_init_pos_) / SOUND_DISTANCE_INTERVAL); +const int PREVIOUS_INDEX = static_cast(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): + ```cpp + last_grounded_position_ = static_cast(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):** + ```cpp + const int FALL_DISTANCE = static_cast(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_`: +```cpp +void Player::switchBorders() { + // ... teleportar jugador ... + setState(State::STANDING); + last_grounded_position_ = static_cast(y_); // ← RESETEAR +} +``` + +**Sonidos - Sistema Basado en Distancia Vertical:** + +Similar al sistema de JUMPING, pero usando los sonidos de caída: + +```cpp +// Cálculo del índice de sonido +const int SOUND_INDEX = static_cast((y_ - last_grounded_position_) / SOUND_DISTANCE_INTERVAL); +const int PREVIOUS_INDEX = static_cast((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 | ✅ Sí | 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_) + +```cpp +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_) + +```cpp +std::array 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:** +```cpp +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_) + +```cpp +std::array 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_) + +```cpp +std::array 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 + +```cpp +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 + +```cpp +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 + +```cpp +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 + +```cpp +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:** +```cpp +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:** +```cpp +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:** +```cpp +struct LineVertical { + int x; // Posición X de la línea + int y1; // Y inicial + int y2; // Y final +}; +``` + +**Uso típico:** +```cpp +const LineVertical SIDE = { + .x = static_cast(x_), + .y1 = static_cast(y_) + HEIGHT - 2, + .y2 = static_cast(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:** +```cpp +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:** +```cpp +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:** +```cpp +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: + ```cpp + 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(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: + +```cpp +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) + +```cpp +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 + +```cpp +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) + +```cpp +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: + +```cpp +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 + +```cpp +// 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) + +```cpp +// ==================== 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(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(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(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(x_) + : static_cast(x_) + WIDTH - 1; + LineVertical side = {side_x, static_cast(y_) + HEIGHT - 2, + static_cast(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(last_grounded_position_)); + const int SOUND_INDEX = static_cast(DISTANCE_FROM_START / SOUND_DISTANCE_INTERVAL); + + // Calcular índice previo (frame anterior) + const float PREV_DISTANCE = std::abs(y_prev_ - static_cast(last_grounded_position_)); + const int PREVIOUS_INDEX = static_cast(PREV_DISTANCE / SOUND_DISTANCE_INTERVAL); + + // Solo reproduce cuando cambia de índice (nuevo hito alcanzado) + if (SOUND_INDEX != PREVIOUS_INDEX && SOUND_INDEX < static_cast(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(last_grounded_position_); + const int SOUND_INDEX = static_cast(DISTANCE_FALLEN / SOUND_DISTANCE_INTERVAL); + + // Calcular índice previo (frame anterior) + const float PREV_DISTANCE = y_prev_ - static_cast(last_grounded_position_); + const int PREVIOUS_INDEX = static_cast(PREV_DISTANCE / SOUND_DISTANCE_INTERVAL); + + // Solo reproduce cuando cambia de índice + if (SOUND_INDEX != PREVIOUS_INDEX && SOUND_INDEX < static_cast(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 diff --git a/PLAYER_RULES.md b/PLAYER_RULES.md new file mode 100644 index 0000000..4883162 --- /dev/null +++ b/PLAYER_RULES.md @@ -0,0 +1,49 @@ +# Reglas para la clase Player + +## Estados + +El jugador tiene tres estados diferenciados: +- STANDING +- JUMPING +- FALLING + +## Puntos de colision + +El jugador tiene un rectabgulo que lo delimita. Se obtiene con getRect() +Tiene ademas dos puntos en un vector de puntos llamado feet_ con el punto inferior izquierdo y el inferior derecho +Tiene otros dos puntos llamados under_feet_ que son los puntos inmediatamente inferiores a los puntos en feet_. Se usan para saber SOBRE qué tipo de superficie esta el jugador o si bajo solo tiene aire +Tiene otros 8 puntos (el jugador ocupa dos tiles en vertical del mapa, los tiles son de 8 pixeles) que son las 4 esquinas de los dos rectangulos de 8px que formarian al jugador. Se usa para comprobar si el jugador está tocando algun tile de los que matan + +## Comprobar colisiones con el mapa +Esto es un poco marciano pero, el jugador genera un rectangulo de proyeccion. Digamos que si se va a mover 3 pixeles hacia la derecha, el rectangulo de proyeccion es el rectangulo que cubre la superficie entre donde está el jugador y donde estará el jugador. Se usa para ver si ese rectangulo (que suele ser de ancho o alto 1, ya que el jugador no llega a moverse mas de un pixel cada vez) colisiona con alguna superficie del mapa. En caso de colision se devuelve el punto de colision y se recoloca al jugador en ese punto. Eso para las superficies rectas. + +## Reglas +A continuacion las reglas que se han de cumplir para el jugador en todo momento +- Si está STANDING -> vy_ = 0; +- Si no tiene suelo debajo y no está JUMPING -> FALLING +- Si está JUMPING y tropieza contra el techo -> FALLING +- Si está FALLING -> vx_ = 0 +- Si está STANDING y tropieza lateralmente con una Slope, se pega a la slope +- Si esta FALLING y tiene una Slope bajo los pies, se pega a la slope +- La unica forma de atravesar una Slope es en estado JUMPING y con vx_ != 0 +- Si salta, se guarda la posicion inicial en Y, si durante el salto está mas bajo de Y (es decir, el valor de y pasa a ser superior al inicial) -> FALLING + +## Tipos de superficies +Hay tres tipos de superficies: +- Suelo normal +- Suelo tipo conveyor belt +- Rampas + +## Reglas explicadas de manera mas abierta +Debido a que las reglas anteriores pueden sonar confusas y ser incompletas, se describen aqui con lenguaje mas natural: +- El jugador camina sobre las superficies +- Si tiene los dos pies sobre el aire -> cae +- El jugador sube y baja por las rampas +- La unica forma de atravesar las rampas es saltando sobre ellas en movimiento +- Un salto recto o una caida (las caidas siempre son rectas) hacen aterrizar al jugador sobre las rampas +- Las conveyor belt desplazan al jugador en la direccion de la conveyor belt +- El jugador cuando esta siendo desplazado por una conveyor belt, no puede cambiar de direccion +- Solo puede saltar y la propia inercia le hace saltar en movimiento +- Hay una excepcion en las conveyor belts y es que cuando el jugador cae sobre ellas, puede mantener la direccion que tenia el jugador. En el momento que el jugador deja de estar pulsando una direccion, ya se acopla al movimiento de la conveyor belt y no puede cambiar de direccion +- El jugador deja de estar ligado al movimiento de la conveyor belt cuando sus dos pies ya no la tienen debajo, bien porque hay suelo o bien porque hay aire -> cae +- El jugador no puede cambiar de direccion en el aire diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 9cc41b1..989119d 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -35,61 +35,42 @@ void Player::render() { // Actualiza las variables del objeto void Player::update(float delta_time) { if (!is_paused_) { - checkInput(); // Comprueba las entradas y establece las velocidades y el flip - move(delta_time); // Recalcula la posición del jugador - animate(delta_time); // Establece la animación del jugador - updateState(delta_time); // Comprueba el estado del jugador - checkBorders(); // Comprueba si está situado en alguno de los cuatro bordes de la habitación - checkJumpEnd(); // Comprueba si ha finalizado el salto al alcanzar la altura de inicio - checkKillingTiles(); // Comprueba que el jugador no toque ningun tile de los que matan} + // 1. Procesamiento de entrada: captura las intenciones del jugador + checkInput(); + // 2. Física: aplica gravedad y actualiza velocidades + applyGravity(delta_time); + updateVelocity(); + + // 3. Movimiento: ejecuta movimiento, actualiza estado durante movimiento, resuelve colisiones + move(delta_time); + + // 4. Finalización: animación, comprobaciones y efectos + animate(delta_time); + checkBorders(); + checkJumpEnd(); + checkKillingTiles(); setColor(); } } -// Comprueba las entradas y establece las velocidades y el flip +// Comprueba las entradas y establece las banderas de intención void Player::checkInput() { - // Solo comprueba las entradas de dirección cuando está sobre una superficie - if (state_ != State::STANDING) { - return; - } - - if (!auto_movement_) { - // Comprueba las entradas de desplazamiento lateral solo en el caso de no estar enganchado a una superficie automatica - if (Input::get()->checkInput(InputAction::LEFT)) { - vx_ = -HORIZONTAL_VELOCITY; - sprite_->setFlip(SDL_FLIP_HORIZONTAL); - left_or_right_is_pressed_ = true; - } - - else if (Input::get()->checkInput(InputAction::RIGHT)) { - vx_ = HORIZONTAL_VELOCITY; - sprite_->setFlip(SDL_FLIP_NONE); - left_or_right_is_pressed_ = true; - } - - else { - // No se pulsa ninguna dirección - vx_ = 0.0F; - left_or_right_is_pressed_ = false; - } - } else { - // El movimiento lo proporciona la superficie - vx_ = HORIZONTAL_VELOCITY * room_->getAutoSurfaceDirection(); - sprite_->setFlip(vx_ > 0.0F ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL); + // Resetea las banderas de intención + want_to_jump_ = false; + want_to_move_left_ = false; + want_to_move_right_ = false; + + // Captura las intenciones de movimiento + if (Input::get()->checkInput(InputAction::LEFT)) { + want_to_move_left_ = true; + } else if (Input::get()->checkInput(InputAction::RIGHT)) { + want_to_move_right_ = true; } + // Captura la intención de salto if (Input::get()->checkInput(InputAction::JUMP)) { - /* - Solo puede saltar si ademas de estar (State::STANDING) - Esta sobre el suelo, rampa o suelo que se mueve - Esto es para evitar el salto desde el vacio al cambiar de pantalla verticalmente - Ya que se coloca el estado State::STANDING al cambiar de pantalla - */ - - if (isOnFloor() || isOnAutoSurface()) { - setState(State::JUMPING); - } + want_to_jump_ = true; } } @@ -120,38 +101,120 @@ void Player::checkBorders() { } } -// Comprueba el estado del jugador +// Actualiza el estado del jugador basado en física e intenciones void Player::updateState(float delta_time) { - // Actualiza las variables en función del estado + // Guarda el estado anterior para detectar transiciones + previous_state_ = state_; + + // Máquina de estados: determina transiciones basándose en PLAYER_RULES.md switch (state_) { case State::STANDING: { - // Calcula la distancia de caída en pixels (velocidad * tiempo) - const int FALLING_DISTANCE = static_cast(y_) - last_grounded_position_; - if (previous_state_ == State::FALLING && FALLING_DISTANCE > MAX_FALLING_HEIGHT) { - // Si cae de muy alto, el jugador muere - is_alive_ = false; + // Comprueba muerte por caída desde altura + if (previous_state_ == State::FALLING) { + const int FALLING_DISTANCE = static_cast(y_) - last_grounded_position_; + if (FALLING_DISTANCE > MAX_FALLING_HEIGHT) { + is_alive_ = false; + } } - vy_ = 0.0F; - jumping_time_ = 0.0F; + + // Actualiza la posición de tierra last_grounded_position_ = static_cast(y_); - auto_movement_ = !isOnFloor() && isOnAutoSurface(); - if (!isOnFloor() && !isOnAutoSurface() && !isOnDownSlope()) { + jumping_time_ = 0.0F; + + // Regla: Si no tiene suelo debajo y no está JUMPING -> FALLING + if (shouldFall()) { setState(State::FALLING); + return; + } + + // Regla: Puede saltar si está sobre suelo o superficie automática + if (want_to_jump_ && canJump()) { + setState(State::JUMPING); + return; + } + + // Nota: auto_movement_ se gestiona en updateVelocity() basado en el input + break; + } + + case State::JUMPING: { + // Actualiza el tiempo de salto + jumping_time_ += delta_time; + playJumpSound(); + + // Regla: Si durante el salto Y > jump_init_pos -> FALLING + if (static_cast(y_) >= jump_init_pos_ && vy_ > 0.0F) { + setState(State::FALLING); + return; } break; } case State::FALLING: { - vx_ = 0.0F; - vy_ = MAX_VY; + // Reproduce sonido de caída playFallSound(); break; } + default: + break; + } +} + +// Comprueba si el jugador puede saltar +auto Player::canJump() -> bool { + // Solo puede saltar si está STANDING y sobre suelo o superficie automática + return state_ == State::STANDING && (isOnFloor() || isOnAutoSurface()); +} + +// Comprueba si el jugador debe caer +auto Player::shouldFall() -> bool { + // Cae si no tiene suelo, no está en superficie automática, y no está en rampa descendente + return !isOnFloor() && !isOnAutoSurface() && !isOnDownSlope(); +} + +// Actualiza velocidad basada en estado e intenciones +void Player::updateVelocity() { + switch (state_) { + case State::STANDING: { + // Regla: Si está STANDING -> vy_ = 0 + vy_ = 0.0F; + + if (!auto_movement_) { + // Movimiento normal: el jugador controla la dirección + 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 { + // No se pulsa ninguna dirección + vx_ = 0.0F; + // Regla conveyor belt: cuando el jugador deja de pulsar, se acopla al movimiento + if (isOnAutoSurface()) { + auto_movement_ = true; + } + } + } else { + // Movimiento automático: conveyor belt controla la dirección + // Regla conveyor belt: el jugador no puede cambiar de dirección + vx_ = HORIZONTAL_VELOCITY * room_->getAutoSurfaceDirection(); + sprite_->setFlip(vx_ > 0.0F ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL); + } + break; + } + case State::JUMPING: { - last_grounded_position_ = static_cast(y_); - jumping_time_ += delta_time; - playJumpSound(); + // Durante el salto, mantiene la velocidad horizontal + // La velocidad vertical la controla applyGravity() + break; + } + + case State::FALLING: { + // Regla: Si está FALLING -> vx_ = 0 (no puede cambiar dirección en el aire) + vx_ = 0.0F; + // La velocidad vertical es MAX_VY (ya configurada por setState) break; } @@ -199,88 +262,78 @@ void Player::applyGravity(float delta_time) { } } -// Maneja el movimiento horizontal hacia la izquierda -void Player::moveHorizontalLeft(float delta_time) { - // Crea el rectangulo de proyección en el eje X para ver si colisiona - const float DISPLACEMENT = vx_ * delta_time; - SDL_FRect proj = { - .x = x_ + DISPLACEMENT, - .y = y_, - .w = std::ceil(std::fabs(DISPLACEMENT)), // Para evitar que tenga un ancho de 0 pixels - .h = HEIGHT}; - - // Comprueba la colisión con las superficies - const int POS = room_->checkRightSurfaces(&proj); - - // Calcula la nueva posición - if (POS == -1) { - // Si no hay colisión - x_ += DISPLACEMENT; - } else { - // Si hay colisión lo mueve hasta donde no colisiona - x_ = POS + 1; - std::cout << "LEFT\n"; +// Maneja el movimiento sobre rampas +// direction: -1 para izquierda, 1 para derecha +void Player::handleSlopeMovement(int direction) { + // No procesa rampas durante el salto (permite atravesarlas cuando salta con movimiento horizontal) + // Pero SÍ procesa en STANDING y FALLING (para pegarse a las rampas) + if (state_ == State::JUMPING) { + return; } - // Si ha tocado alguna rampa mientras camina (sin saltar) - if (state_ == State::STANDING) { - // Si está bajando la rampa, recoloca al jugador - if (isOnDownSlope() && state_ == State::STANDING) { - y_ += 1; - std::cout << "RAMP DOWN LEFT\n"; - } - // Asciende - else { - const LineVertical LEFT_SIDE = {.x = static_cast(x_), .y1 = static_cast(y_) + static_cast(HEIGHT) - 2, .y2 = static_cast(y_) + static_cast(HEIGHT) - 1}; // Comprueba solo los dos pixels de abajo - const int LY = room_->checkLeftSlopes(&LEFT_SIDE); - if (LY > -1) { - y_ = LY - HEIGHT; - std::cout << "RAMP UP LEFT\n"; - } - } + // Regla: Si está bajando la rampa, se pega a la slope + if (isOnDownSlope()) { + y_ += 1; + return; + } + + // Regla: Si está STANDING y tropieza lateralmente con una Slope, se pega a la slope + // Comprueba si hay rampa en contacto lateral (solo los dos pixels inferiores) + const int SIDE_X = direction < 0 ? static_cast(x_) : static_cast(x_) + WIDTH - 1; + const LineVertical SIDE = { + .x = SIDE_X, + .y1 = static_cast(y_) + HEIGHT - 2, + .y2 = static_cast(y_) + HEIGHT - 1 + }; + + // Comprueba la rampa correspondiente según la dirección + const int SLOPE_Y = direction < 0 ? room_->checkLeftSlopes(&SIDE) : room_->checkRightSlopes(&SIDE); + + if (SLOPE_Y > -1) { + // Hay rampa: sube al jugador para pegarlo a la rampa + y_ = SLOPE_Y - HEIGHT; } } -// Maneja el movimiento horizontal hacia la derecha -void Player::moveHorizontalRight(float delta_time) { - // Crea el rectangulo de proyección en el eje X para ver si colisiona +// Maneja el movimiento horizontal +// direction: -1 para izquierda, 1 para derecha +void Player::moveHorizontal(float delta_time, int direction) { const float DISPLACEMENT = vx_ * delta_time; - SDL_FRect proj = { - .x = x_ + WIDTH, - .y = y_, - .w = std::ceil(DISPLACEMENT), // Para evitar que tenga un ancho de 0 pixels - .h = HEIGHT}; - // Comprueba la colisión - const int POS = room_->checkLeftSurfaces(&proj); + // Crea el rectangulo de proyección en el eje X para ver si colisiona + SDL_FRect proj; + if (direction < 0) { + // Movimiento a la izquierda + proj = { + .x = x_ + DISPLACEMENT, + .y = y_, + .w = std::ceil(std::fabs(DISPLACEMENT)), + .h = HEIGHT + }; + } else { + // Movimiento a la derecha + proj = { + .x = x_ + WIDTH, + .y = y_, + .w = std::ceil(DISPLACEMENT), + .h = HEIGHT + }; + } + + // Comprueba la colisión con las superficies + const int POS = direction < 0 ? room_->checkRightSurfaces(&proj) : room_->checkLeftSurfaces(&proj); // Calcula la nueva posición if (POS == -1) { - // Si no hay colisión + // No hay colisión: mueve al jugador x_ += DISPLACEMENT; } else { - // Si hay colisión lo mueve hasta donde no colisiona - x_ = POS - WIDTH; - std::cout << "RIGHT\n"; + // Hay colisión: reposiciona al jugador en el punto de colisión + x_ = direction < 0 ? POS + 1 : POS - WIDTH; } - // Si ha tocado alguna rampa mientras camina (sin saltar) - if (state_ == State::STANDING) { - // Si está bajando la rampa, recoloca al jugador - if (isOnDownSlope() && state_ == State::STANDING) { - y_ += 1; - std::cout << "RAMP DOWN RIGHT\n"; - } - // Asciende - else { - const LineVertical RIGHT_SIDE = {.x = static_cast(x_) + static_cast(WIDTH) - 1, .y1 = static_cast(y_) + static_cast(HEIGHT) - 2, .y2 = static_cast(y_) + static_cast(HEIGHT) - 1}; // Comprueba solo los dos pixels de abajo - const int RY = room_->checkRightSlopes(&RIGHT_SIDE); - if (RY > -1) { - y_ = RY - HEIGHT; - std::cout << "RAMP UP RIGHT\n"; - } - } - } + // Maneja el movimiento sobre rampas + handleSlopeMovement(direction); } // Maneja el movimiento vertical hacia arriba @@ -302,9 +355,9 @@ void Player::moveVerticalUp(float delta_time) { // Si no hay colisión y_ += DISPLACEMENT; } else { - // Si hay colisión lo mueve hasta donde no colisiona y entra en caída + // Si hay colisión lo mueve hasta donde no colisiona + // Regla: Si está JUMPING y tropieza contra el techo -> FALLING y_ = POS + 1; - std::cout << "TOP\n"; setState(State::FALLING); } } @@ -325,47 +378,45 @@ void Player::moveVerticalDown(float delta_time) { if (POS > -1) { // Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie y_ = POS - HEIGHT; - std::cout << "BOTTOM\n"; setState(State::STANDING); + auto_movement_ = false; // Desactiva conveyor belt al aterrizar } else { // Si no hay colisión con los muros, comprueba la colisión con las rampas - if (state_ != State::JUMPING) { // Las rampas no se miran si se está saltando - auto frect = getRect(); - auto rect = toSDLRect(frect); + // Regla: La unica forma de atravesar una Slope es en estado JUMPING y con vx_ != 0 + if (state_ != State::JUMPING || vx_ == 0.0F) { + // No está saltando o salta recto: se pega a las rampas + auto rect = toSDLRect(proj); const LineVertical LEFT_SIDE = {.x = rect.x, .y1 = rect.y, .y2 = rect.y + rect.h - 1}; const LineVertical RIGHT_SIDE = {.x = rect.x + rect.w - 1, .y1 = rect.y, .y2 = rect.y + rect.h - 1}; - std::cout << RIGHT_SIDE.x << " " << RIGHT_SIDE.y1 << " " << RIGHT_SIDE.y2 << "\n"; const float POINT = std::max(room_->checkRightSlopes(&RIGHT_SIDE), room_->checkLeftSlopes(&LEFT_SIDE)); if (POINT > -1) { - // No está saltando y hay colisión con una rampa - // Calcula la nueva posición + // Hay colisión con una rampa: se pega a ella y_ = POINT - HEIGHT; - std::cout << "BOTTOM SLOPE\n"; setState(State::STANDING); } else { - // No está saltando y no hay colisón con una rampa - // Calcula la nueva posición + // No hay colisión con rampa: continúa cayendo y_ += DISPLACEMENT; } } else { - // Esta saltando y no hay colisión con los muros - // Calcula la nueva posición + // Está saltando con movimiento horizontal: atraviesa las rampas y_ += DISPLACEMENT; } } } -// Recalcula la posición del jugador y su animación +// Orquesta el movimiento del jugador void Player::move(float delta_time) { - applyGravity(delta_time); // Aplica gravedad al jugador - // Movimiento horizontal if (vx_ < 0.0F) { - moveHorizontalLeft(delta_time); + moveHorizontal(delta_time, -1); // Izquierda } else if (vx_ > 0.0F) { - moveHorizontalRight(delta_time); + moveHorizontal(delta_time, 1); // Derecha } + // Actualización de estado DURANTE el movimiento (después de horizontal, antes de vertical) + // Esto asegura que el estado se actualice con la posición correcta + updateState(delta_time); + // Movimiento vertical if (vy_ < 0.0F) { moveVerticalUp(delta_time); @@ -373,7 +424,8 @@ void Player::move(float delta_time) { moveVerticalDown(delta_time); } - placeSprite(); // Coloca el sprite en la nueva posición + // Actualiza la geometría del collider y sprite + updateColliderGeometry(); } // Establece la animación del jugador @@ -545,42 +597,37 @@ void Player::updateFeet() { // Cambia el estado del jugador void Player::setState(State value) { + // Solo actualiza el estado y configura las variables iniciales + // NO llama a updateState() para evitar recursión circular previous_state_ = state_; state_ = value; switch (state_) { case State::STANDING: - vy_ = 0.0F; - last_grounded_position_ = static_cast(y_); - std::cout << "SET STANDING\n"; + // Se establecerá vy_ = 0 en updateVelocity() break; case State::JUMPING: - last_grounded_position_ = static_cast(y_); + // Configura el salto vy_ = JUMP_VELOCITY; jump_init_pos_ = y_; jumping_time_ = 0.0F; - std::cout << "SET JUMPING\n"; break; case State::FALLING: - vx_ = 0.0F; + // Configura la caída vy_ = MAX_VY; - playFallSound(); + // vx_ = 0 se establecerá en updateVelocity() if (previous_state_ == State::STANDING) { last_grounded_position_ = static_cast(y_); } auto_movement_ = false; jumping_time_ = 0.0F; - std::cout << "SET FALLING\n"; break; default: break; } - - // Llama a checkState con delta_time 0 porque esto es un cambio de estado inmediato - updateState(0.0F); } // Inicializa los sonidos de salto y caida @@ -619,9 +666,14 @@ void Player::initSprite(const std::string& animations_path) { sprite_->setCurrentAnimation("walk"); } -// Coloca el sprite en la posición del jugador -void Player::placeSprite() { - sprite_->setPos(x_, y_); // Recoloca el sprite +// Actualiza collider_box y collision points +void Player::updateColliderGeometry() { + placeSprite(); // Coloca el sprite en la posición del jugador collider_box_ = getRect(); // Actualiza el rectangulo de colisión updateColliderPoints(); // Actualiza los puntos de colisión +} + +// Coloca el sprite en la posición del jugador +void Player::placeSprite() { + sprite_->setPos(x_, y_); } \ No newline at end of file diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 2b1c3e3..4867fd0 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -98,53 +98,83 @@ class Player { std::shared_ptr room_; // Objeto encargado de gestionar cada habitación del juego std::unique_ptr sprite_; // Sprite del jugador - // --- Variables --- + // --- Variables de posición y física --- float x_ = 0.0F; // Posición del jugador en el eje X float y_ = 0.0F; // Posición del jugador en el eje Y float vx_ = 0.0F; // Velocidad/desplazamiento del jugador en el eje X float vy_ = 0.0F; // Velocidad/desplazamiento del jugador en el eje Y - Uint8 color_ = 0; // Color del jugador + + // --- Variables de estado --- + State state_ = State::STANDING; // Estado en el que se encuentra el jugador. Util apara saber si está saltando o cayendo + State previous_state_ = State::STANDING; // Estado previo en el que se encontraba el jugador + + // --- Variables de entrada (input intent) --- + bool want_to_jump_ = false; // Indica si el jugador quiere saltar + bool want_to_move_left_ = false; // Indica si el jugador quiere moverse a la izquierda + bool want_to_move_right_ = false; // Indica si el jugador quiere moverse a la derecha + + // --- Variables de colisión --- SDL_FRect collider_box_; // Caja de colisión con los enemigos u objetos std::array collider_points_{}; // Puntos de colisión con el mapa std::array under_feet_{}; // Contiene los puntos que hay bajo cada pie del jugador std::array feet_{}; // Contiene los puntos que hay en el pie del jugador - State state_ = State::STANDING; // Estado en el que se encuentra el jugador. Util apara saber si está saltando o cayendo - State previous_state_ = State::STANDING; // Estado previo en el que se encontraba el jugador + + // --- Variables de juego --- bool is_on_border_ = false; // Indica si el jugador esta en uno de los cuatro bordes de la pantalla bool is_alive_ = true; // Indica si el jugador esta vivo o no bool is_paused_ = false; // Indica si el jugador esta en modo pausa bool auto_movement_ = false; // Indica si esta siendo arrastrado por una superficie automatica Room::Border border_ = Room::Border::TOP; // Indica en cual de los cuatro bordes se encuentra int jump_init_pos_ = 0; // Valor del eje Y en el que se inicia el salto + int last_grounded_position_ = 0; // Ultima posición en Y en la que se estaba en contacto con el suelo + + // --- Variables de renderizado y sonido --- + Uint8 color_ = 0; // Color del jugador std::vector jumping_sound_; // Vecor con todos los sonidos del salto std::vector falling_sound_; // Vecor con todos los sonidos de la caída float jumping_time_ = 0.0F; // Tiempo acumulado de salto en segundos - int last_grounded_position_ = 0; // Ultima posición en Y en la que se estaba en contacto con el suelo - bool left_or_right_is_pressed_ = false; // Indica si se está pulsando una de las dos direcciones. Sirve para las conveyor belts - // --- Funciones --- - void checkInput(); // Comprueba las entradas y modifica variables - void checkBorders(); // Comprueba si se halla en alguno de los cuatro bordes - void updateState(float delta_time); // Comprueba el estado del jugador + // --- Funciones de inicialización --- + void initSprite(const std::string& animations_path); // Inicializa el sprite del jugador + void initSounds(); // Inicializa los sonidos de salto y caida + void applySpawnValues(const SpawnData& spawn); // Aplica los valores de spawn al jugador + + // --- Funciones de procesamiento de entrada --- + void checkInput(); // Comprueba las entradas y establece las banderas de intención + + // --- Funciones de gestión de estado --- + void updateState(float delta_time); // Actualiza el estado del jugador basado en física e intenciones + void setState(State value); // Cambia el estado del jugador + auto canJump() -> bool; // Comprueba si el jugador puede saltar + auto shouldFall() -> bool; // Comprueba si el jugador debe caer + + // --- Funciones de física --- void applyGravity(float delta_time); // Aplica gravedad al jugador - void move(float delta_time); // Recalcula la posición del jugador y su animación - void moveHorizontalLeft(float delta_time); // Maneja el movimiento horizontal hacia la izquierda - void moveHorizontalRight(float delta_time); // Maneja el movimiento horizontal hacia la derecha + void updateVelocity(); // Actualiza velocidad basada en estado e intenciones + + // --- Funciones de movimiento y colisión --- + void move(float delta_time); // Orquesta el movimiento del jugador + void moveHorizontal(float delta_time, int direction); // Maneja el movimiento horizontal (-1: izq, 1: der) void moveVerticalUp(float delta_time); // Maneja el movimiento vertical hacia arriba void moveVerticalDown(float delta_time); // Maneja el movimiento vertical hacia abajo - void animate(float delta_time); // Establece la animación del jugador - void checkJumpEnd(); // Comprueba si ha finalizado el salto al alcanzar la altura de inicio - void playJumpSound(); // Calcula y reproduce el sonido de salto - void playFallSound(); // Calcula y reproduce el sonido de caer + void handleSlopeMovement(int direction); // Maneja el movimiento sobre rampas + + // --- Funciones de detección de superficies --- auto isOnFloor() -> bool; // Comprueba si el jugador tiene suelo debajo de los pies auto isOnAutoSurface() -> bool; // Comprueba si el jugador esta sobre una superficie automática auto isOnDownSlope() -> bool; // Comprueba si el jugador está sobre una rampa hacia abajo - auto checkKillingTiles() -> bool; // Comprueba que el jugador no toque ningun tile de los que matan + + // --- Funciones de actualización de geometría --- + void updateColliderGeometry(); // Actualiza collider_box y collision points void updateColliderPoints(); // Actualiza los puntos de colisión void updateFeet(); // Actualiza los puntos de los pies - void setState(State value); // Cambia el estado del jugador - void initSounds(); // Inicializa los sonidos de salto y caida void placeSprite(); // Coloca el sprite en la posición del jugador - void applySpawnValues(const SpawnData& spawn); // Aplica los valores de spawn al jugador - void initSprite(const std::string& animations_path); // Inicializa el sprite del jugador + + // --- Funciones de finalización --- + void animate(float delta_time); // Establece la animación del jugador + void checkBorders(); // Comprueba si se halla en alguno de los cuatro bordes + void checkJumpEnd(); // Comprueba si ha finalizado el salto al alcanzar la altura de inicio + auto checkKillingTiles() -> bool; // Comprueba que el jugador no toque ningun tile de los que matan + void playJumpSound(); // Calcula y reproduce el sonido de salto + void playFallSound(); // Calcula y reproduce el sonido de caer }; \ No newline at end of file