1530 lines
51 KiB
Markdown
1530 lines
51 KiB
Markdown
# 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<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):
|
|
```cpp
|
|
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):**
|
|
```cpp
|
|
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_`:
|
|
```cpp
|
|
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:
|
|
|
|
```cpp
|
|
// 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 | ✅ 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<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:**
|
|
```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<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_)
|
|
|
|
```cpp
|
|
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
|
|
|
|
```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<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:**
|
|
```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<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:
|
|
|
|
```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<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
|